Введение в проблему сравнения объектов в Python и библиотеке attrs
Представьте ситуацию: вы пишете сервис синхронизации конфигураций, а тесты внезапно начинают падать просто потому, что у двух идентичных серверов различаются таймстампы последнего пинга. Знакомая боль? Когда стандартная проверка == начинает рушить бизнес-логику из-за служебных полей, на помощь приходит кастомизация (примерно так же мы пытаемся игнорировать баги в легаси-коде, надеясь, что они исчезнут сами). К счастью, библиотека attrs (и её концептуальный наследник в стандартной библиотеке — dataclasses) создана не только для того, чтобы избавлять нас от написания шаблонного кода для __init__, __repr__ и __eq__, но и для тонкой настройки.
По умолчанию attrs генерирует метод сравнения, возвращающий True только тогда, когда оба экземпляра принадлежат одному классу, а значения всех их полей попарно равны. Но на практике этого часто не хватает. Что делать, если экземпляры должны считаться равными по бизнес-логике, даже если часть их полей различается? Как исключить служебные поля (вроде таймстампов или счетчиков попыток) из операции ==? В этой статье мы разберем, как устроен механизм равенства в attrs, и рассмотрим продвинутые паттерны кастомизации сравнения объектов.
Как работает автоматическая генерация сравнения в attrs
Чтобы понять, как обуздать сравнение объектов, заглянем под капот автогенерации. Когда вы декорируете класс с помощью @define или @attr.s, библиотека собирает все поля, помеченные через attr.ib() или аннотации типов. По умолчанию параметр eq равен True для каждого из них.
Рассмотрим базовый пример:
import attr
@attr.s
class ServerConfig:
host = attr.ib()
port = attr.ib()
timeout = attr.ib()
config1 = ServerConfig("localhost", 8080, 30)
config2 = ServerConfig("localhost", 8080, 30)
print(config1 == config2) # Выведет True
Если требуется исключить определенный атрибут из сравнения (например, время последнего подключения или метаданные трассировки), достаточно передать параметр eq=False при объявлении поля:
@attr.s
class FlexibleConfig:
host = attr.ib()
port = attr.ib()
timeout = attr.ib(eq=False) # Это поле игнорируется при сравнении
c1 = FlexibleConfig("127.0.0.1", 5432, 10)
c2 = FlexibleConfig("127.0.0.1", 5432, 60)
print(c1 == c2) # Выведет True, несмотря на разный timeout!
Но что делать, если исключить поле недостаточно и правила равенства начинают диктоваться более сложными условиями?
Продвинутая кастомизация: когда встроенных параметров недостаточно
Иногда логика равенства зависит от внешнего контекста или требует сложных вычислений, которые нельзя описать простым флагом eq=False. В таких случаях стандартный подход attrs можно переопределить, написав собственный метод __eq__ внутри класса (и молиться, чтобы вы не нарушили контракт транзитивности, иначе Stack Overflow будет вашим лучшим другом до конца недели).
Библиотека attrs позволяет комбинировать автоматическую генерацию кода с ручной доработкой. Если вы реализуете метод __eq__ вручную, Python будет использовать вашу логику вместо сгенерированной автоматически:
@attr.s
class CustomEqualConfig:
host = attr.ib()
port = attr.ib()
allowed_envs = attr.ib(default=attr.Factory(list))
def __eq__(self, other):
if not isinstance(other, CustomEqualConfig):
return NotImplemented
# Сравниваем только хост и порт, игнорируя порядок в списках сред
return self.host == other.host and self.port == other.port
Заключение
Библиотека attrs предоставляет гибкие инструменты для управления равенством объектов. Для простых сценариев достаточно использовать параметр eq=False на уровне конкретных полей. Если же бизнес-логика требует нестандартных проверок, ничто не мешает переопред