Элегантные способы поддержки эквивалентности ("равенства") в классах Python
<п>При написании пользовательских классов часто важно обеспечить эквивалентность с помощью операторов ==
и !=
. В Python это возможно благодаря реализации специальных методов eq
и ne
соответственно. Наилучший способ, который я нашел для этой задачи, выглядит следующим образом:</п>
class Foo:
def __init__(self, item):
self.item = item
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
<п>Есть ли у вас более элегантные способы решения этой задачи? Известны ли вам какие-либо конкретные недостатки использования приведенного выше метода сравнения dict
?</п>
<п>Примечание: Для ясности — когда eq
и ne
не определены, вы получите следующее поведение:</п>
>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False
<п>То есть a == b
возвращает False
, потому что на самом деле выполняется a is b
, проверка идентичности (т.е. "Являются ли a
и b
одним и тем же объектом?").</п>
<п>Когда eq
и ne
определены, вы получите следующее поведение (которое мы и хотим достичь):</п>
>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
5 ответ(ов)
Вы правы, с наследованием нужно быть осторожным. В вашем примере, когда вы используете isinstance(other, self.__class__)
в методе __eq__
, это может привести к неожиданным результатам. Рассмотрим ваш код:
class Foo:
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False
class Bar(Foo): pass
b = Bar()
f = Foo()
print(f == b) # True
print(b == f) # False
Как видно, сравнение f == b
возвращает True
, что может быть неожиданным, поскольку экземпляры разных классов должны быть не равны.
Чтобы избежать этой путаницы, рекомендуется использовать более строгую проверку типов, например:
def __eq__(self, other):
if type(other) is type(self):
return self.__dict__ == other.__dict__
return False
Таким образом, вы будете уверены, что два объекта равны только если они экземпляры одного и того же класса. Однако ваш изначальный подход также работает, поскольку специальные методы, такие как __eq__
, предназначены именно для этой цели. Но нужна осторожность, чтобы избежать неожиданных результатов при работе с наследованием.
Вы описали способ, который я всегда использовал. Поскольку он абсолютно универсален, вы можете выделить эту функциональность в отдельный класс-примеситель (mixin) и наследовать его в тех классах, где вам требуется такая функциональность.
class CommonEqualityMixin(object):
def __eq__(self, other):
return (isinstance(other, self.__class__)
and self.__dict__ == other.__dict__)
def __ne__(self, other):
return not self.__eq__(other)
class Foo(CommonEqualityMixin):
def __init__(self, item):
self.item = item
Такое решение позволит вам легко переиспользовать логику сравнения объектов в любых классах, просто унаследовав от вашего CommonEqualityMixin
.
Вы не обязаны переопределять как __eq__
, так и __ne__
, вы можете переопределить только __cmp__
, но это повлияет на результат операторов ==
, !=
, <
, >
и так далее.
Оператор is
проверяет идентичность объектов. Это означает, что выражение a is b
будет истинно только в случае, если a
и b
ссылаются на один и тот же объект. В Python вы всегда храните ссылку на объект в переменной, а не сам объект, поэтому для того, чтобы a is b
оказался истинным, объекты должны находиться по одному и тому же адресу в памяти. Но зачем и, что более важно, почему вы хотели бы переопределить такое поведение?
Дополнение: я не знал, что __cmp__
был удалён в Python 3, поэтому избегайте его использования.
Для того чтобы продемонстрировать разницу, обратите внимание на следующий вопрос: следует ли определять метод __ne__
, исходя из метода __eq__
, или же лучше использовать сравнение с помощью оператора ==
? В ответе на ваш вопрос (https://stackoverflow.com/a/30676267/541136) я показал, что правильнее использовать следующее определение:
def __ne__(self, other):
return not self == other
Такой подход предпочтительнее, чем определение __ne__
через __eq__
:
def __ne__(self, other):
return not self.__eq__(other)
Это связано с тем, что использование not self == other
лучше сохраняет инкапсуляцию и позволяет избежать потенциальных проблем с рекурсией, если __eq__
будет переопределен ненадлежащим образом.
Думаю, что два термина, которые вам нужны, это равенство (==) и идентичность (is). Например:
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True <-- a и b имеют равные значения
>>> a is b
False <-- a и b не являются одним и тем же объектом списка
Почему сравнение строк с помощью '==' и 'is' иногда дает разные результаты?
Сравнение строк в Python: is vs. ==
Как клонировать список, чтобы он не изменялся неожиданно после присваивания?
Преобразование списка словарей в DataFrame pandas
Ошибка: "'dict' объект не имеет метода 'iteritems'"