При написании пользовательских классов часто важно разрешить эквивалентность с помощью операторов == и !=. В 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
Я думаю, что принятый ответ будет исправлен или переназначен на ответ Алгориаса, чтобы была реализована строгая проверка типа.
Также убедитесь, что хэш переопределен stackoverflow.com/questions/1608842/…






Вам не нужно переопределять и __eq__, и __ne__, вы можете переопределить только __cmp__, но это повлияет на результат ==,! ==, <,> и так далее.
is проверяет идентичность объекта. Это означает, что is b будет True в случае, когда a и b оба содержат ссылку на один и тот же объект. В python вы всегда держите ссылку на объект в переменной, а не на фактическом объекте, поэтому, по сути, для того, чтобы a is b было истинным, объекты в них должны находиться в одной и той же области памяти. Как и, что наиболее важно, зачем вам отменять такое поведение?
Обновлено: я не знал, что __cmp__ был удален из python 3, поэтому избегайте этого.
Потому что иногда у вас есть другое определение равенства для ваших объектов.
оператор is дает вам ответ интерпретатора на идентичность объекта, но вы по-прежнему можете выразить свое мнение о равенстве, переопределив cmp
В Python 3 «функция cmp () исчезла, а специальный метод __cmp __ () больше не поддерживается». is.gd/aeGv
Я думаю, что вам нужны два термина: равенство (==) и личность (is). Например:
>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True <-- a and b have values which are equal
>>> a is b
False <-- a and b are not the same list object
Возможно, за исключением того, что можно создать класс, который сравнивает только первые два элемента в двух списках, и если эти элементы равны, он оценивает значение True. Я считаю, что это эквивалентность, а не равенство. Совершенно верно в экв, все еще.
Однако я согласен с тем, что «есть» - это проверка идентичности.
То, как вы описываете, - это то, как я всегда это делал. Поскольку он полностью универсален, вы всегда можете разбить эту функциональность на класс миксина и унаследовать ее от классов, в которых вы хотите эту функциональность.
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
+1: шаблон стратегии, позволяющий легко заменять подклассы.
isinstance отстой. Зачем это проверять? Почему не просто self .__ dict__ == other .__ dict__?
@nosklo: Я не понимаю .. что, если два объекта из совершенно несвязанных классов имеют одинаковые атрибуты?
@max nosklo хорошо замечает. При подклассе встроенных объектов учитывайте поведение по умолчанию. Оператору == безразлично, сравниваете ли вы встроенное с подклассом встроенного.
Думал, ноксло предложил пропустить isinstance. В этом случае вы больше не знаете, принадлежит ли other подклассу self.__class__.
@max: да, но не имеет значения, подкласс это или нет. Это не относящаяся к делу информация. Подумайте, что произойдет, если это не подкласс.
@nosklo: если он не подкласс, но случайно имеет те же атрибуты, что и self (как ключи, так и значения), __eq__ может оцениваться как True, хотя это бессмысленно. Я что-нибудь упускаю?
@nosklo: Да, может быть, hasattr(other, '__dict__') and self.__dict__ == other.__dict__ в общем случае подойдет лучше. Думаю, я просто предпочитаю более строгое понятие равенства, учитывая возможность.
Другая проблема со сравнением __dict__ заключается в том, что если у вас есть атрибут, который вы не хотите учитывать в своем определении равенства (скажем, например, уникальный идентификатор объекта или метаданные, такие как отметка времени создания).
@cdleary: Я вижу, что hasattr предотвратит исключение, когда other не имеет __dict__ (т.е. реализован со слотами). Но как это связано с вопросом @nosklo?
Я думаю, реализация этого во всех ваших классах может привести к бесконечной рекурсии с круговыми ссылками (например, установить a.b = b, b.a = a, a == a)
Обратите внимание, что у этого есть некоторые проблемы с наследованием, обязательно проверьте решение это!
Кроме того, отсутствие проверки в hasattr(self, '__dict__') вызовет исключение при сравнении с None. Довольно явная дыра в реализации.
TypeError: нехешируемый тип: 'dict'
С наследованием нужно быть осторожным:
>>> 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()
>>> f == b
True
>>> b == f
False
Проверяйте типы более строго, например:
def __eq__(self, other):
if type(other) is type(self):
return self.__dict__ == other.__dict__
return False
Кроме того, ваш подход будет работать нормально, для этого существуют специальные методы.
Это хороший момент. Я полагаю, стоит отметить, что подклассы встроенных типов по-прежнему допускают равенство в любом направлении, и поэтому проверка того, что это один и тот же тип, может быть даже нежелательна.
Я бы предложил вернуть NotImplemented, если типы разные, делегируя сравнение правым.
@max сравнение не обязательно выполняется слева (LHS) с правой (RHS), затем с RHS на LHS; см. stackoverflow.com/a/12984987/38140. Тем не менее, возврат NotImplemented, как вы предлагаете, всегда будет вызывать superclass.__eq__(subclass), что является желаемым поведением.
это тоже быстрее, потому что isinstance может быть немного медленным
Если у вас много участников и не так много копий объектов, то обычно хорошо добавить начальный тест идентичности if other is self. Это позволяет избежать более длинного сравнения словарей и может дать огромную экономию, когда объекты используются в качестве ключей словаря.
И не забудьте реализовать __hash__()
Я думаю, что это должен быть принятый ответ, потому что он фактически отвечает на конкретный вопрос о __dict__.
Важно отметить, что для того, чтобы равенство было истинным, объекты не обязательно должны быть одного и того же типа. Например, можно утверждать, что связанный список с теми же элементами, что и список, поддерживаемый массивом, на самом деле равны.
Тест is будет проверять идентичность с помощью встроенной функции id (), которая по существу возвращает адрес памяти объекта и, следовательно, не является перегружаемой.
Однако в случае проверки равенства класса вы, вероятно, захотите быть немного более строгим в своих тестах и сравнивать только атрибуты данных в своем классе:
import types
class ComparesNicely(object):
def __eq__(self, other):
for key, value in self.__dict__.iteritems():
if (isinstance(value, types.FunctionType) or
key.startswith("__")):
continue
if key not in other.__dict__:
return False
if other.__dict__[key] != value:
return False
return True
Этот код будет сравнивать только элементы данных, не являющиеся функциями вашего класса, а также пропускать что-либо частное, что обычно является тем, что вы хотите. В случае с обычными старыми объектами Python у меня есть базовый класс, который реализует __init__, __str__, __repr__ и __eq__, поэтому мои объекты POPO не несут бремя всей этой дополнительной (и в большинстве случаев идентичной) логики.
Немного придирчиво, но тесты «есть» с использованием id () только в том случае, если вы не определили свою собственную функцию-член is_ () (2.3+). [docs.python.org/library/operator.html]
Я предполагаю, что под «переопределением» вы на самом деле имеете в виду исправление модуля оператора. В этом случае ваше утверждение не совсем точное. Модуль операторов предоставляется для удобства, и переопределение этих методов не влияет на поведение оператора is. Сравнение с использованием «is» всегда использует id () объекта для сравнения, это поведение не может быть изменено. Также функция-член is_ не влияет на сравнение.
mcrute - я заговорил слишком рано (и неправильно), вы абсолютно правы.
Это очень хорошее решение, особенно когда __eq__ будет объявлен в CommonEqualityMixin (см. Другой ответ). Я нашел это особенно полезным при сравнении экземпляров классов, производных от Base в SQLAlchemy. Чтобы не сравнивать _sa_instance_state, я заменил key.startswith("__")): на key.startswith("_")):. У меня также были некоторые обратные ссылки, и ответ от Algorias генерировал бесконечную рекурсию. Поэтому я назвал все обратные ссылки, начиная с '_', чтобы они также пропускались при сравнении. ПРИМЕЧАНИЕ: в Python 3.x измените iteritems() на items().
@mcrute Обычно __dict__ экземпляра не имеет ничего, что начинается с __, если это не было определено пользователем. Такие вещи, как __class__, __init__ и т. д., Находятся не в __dict__ экземпляра, а в его классе __dict__. OTOH, частные атрибуты могут легко начинаться с __ и, вероятно, должны использоваться для __eq__. Не могли бы вы уточнить, чего именно вы пытались избежать при пропуске атрибутов с префиксом __?
Это не прямой ответ, но он казался достаточно актуальным, чтобы его можно было добавить, поскольку он иногда избавляет от многословной скуки. Вырезано прямо из документации ...
functools.total_ordering (cls)
Учитывая класс, определяющий один или несколько методов упорядочивания с расширенными возможностями сравнения, этот декоратор класса предоставляет все остальное. Это упрощает работу, связанную с указанием всех возможных операций расширенного сравнения:
Класс должен определять один из __lt__(), __le__(), __gt__() или __ge__(). Кроме того, класс должен предоставить метод __eq__().
Новое в версии 2.7
@total_ordering
class Student:
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))
Однако у total_ordering есть небольшие подводные камни: regebro.wordpress.com/2010/12/13/…. Будьте внимательны!
Рассмотрим эту простую задачу:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Итак, Python по умолчанию использует идентификаторы объектов для операций сравнения:
id(n1) # 140400634555856
id(n2) # 140400634555920
Кажется, что переопределение функции __eq__ решает проблему:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return False
n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3
В Python 2 всегда не забывайте также переопределить функцию __ne__, поскольку документация утверждает:
There are no implied relationships among the comparison operators. The truth of
x==ydoes not imply thatx!=yis false. Accordingly, when defining__eq__(), one should also define__ne__()so that the operators will behave as expected.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
В Python 3 в этом больше нет необходимости, поскольку документация утверждает:
By default,
__ne__()delegates to__eq__()and inverts the result unless it isNotImplemented. There are no other implied relationships among the comparison operators, for example, the truth of(x<y or x==y)does not implyx<=y.
Но это не решает всех наших проблем. Давайте добавим подкласс:
class SubNumber(Number):
pass
n3 = SubNumber(1)
n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False
Примечание: Python 2 имеет два типа классов:
Классы классический стиль (или Старый стиль), которые наследуют нет от object и объявлены как class A:, class A(): или class A(B):, где B - класс классического стиля;
Классы новый стиль, которые наследуются от object и объявлены как class A(object) или class A(B):, где B - это класс нового стиля. В Python 3 есть только классы нового стиля, которые объявлены как class A:, class A(object): или class A(B):.
Для классов классического стиля операция сравнения всегда вызывает метод первого операнда, а для классов нового стиля всегда вызывает метод операнда подкласса, независимо от порядка операндов.
Итак, если Number - это класс в классическом стиле:
n1 == n3 вызывает n1.__eq__;n3 == n1 вызывает n3.__eq__;n1 != n3 вызывает n1.__ne__;n3 != n1 вызывает n3.__ne__.И если Number - это класс нового стиля:
n1 == n3, так и n3 == n1 называют n3.__eq__;n1 != n3, так и n3 != n1 называют n3.__ne__.Чтобы исправить проблему некоммутативности операторов == и != для классов классического стиля Python 2, методы __eq__ и __ne__ должны возвращать значение NotImplemented, когда тип операнда не поддерживается. документация определяет значение NotImplemented как:
Numeric methods and rich comparison methods may return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true.
В этом случае оператор делегирует операцию сравнения отраженный метод операнда Другие. документация определяет отраженные методы как:
There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather,
__lt__()and__gt__()are each other’s reflection,__le__()and__ge__()are each other’s reflection, and__eq__()and__ne__()are their own reflection.
Результат выглядит так:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is NotImplemented:
return NotImplemented
return not x
Возвращение значения NotImplemented вместо False является правильным решением даже для классов нового стиля, если коммутативность операторов == и != желательно, когда операнды не связаны между собой (без наследования).
Мы уже на месте? Не совсем. Сколько у нас уникальных номеров?
len(set([n1, n2, n3])) # 3 -- oops
Наборы используют хеши объектов, и по умолчанию Python возвращает хеш идентификатора объекта. Попробуем отменить это:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
Конечный результат выглядит так (я добавил несколько утверждений в конце для проверки):
class Number:
def __init__(self, number):
self.number = number
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is not NotImplemented:
return not x
return NotImplemented
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
class SubNumber(Number):
pass
n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)
assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1
assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1
assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1
assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
hash(tuple(sorted(self.__dict__.items()))) не будет работать, если среди значений self.__dict__ есть какие-либо нехешируемые объекты (то есть, если какой-либо из атрибутов объекта установлен, например, на list).
Верно, но тогда, если у вас есть такие изменяемые объекты в вашем vars (), два объекта на самом деле не равны ...
Отличное резюме, но ты должен реализовать __ne__ с использованием == вместо __eq__.
Три замечания: 1. В Python 3 больше нет необходимости реализовывать __ne__: «По умолчанию __ne__() делегирует __eq__() и инвертирует результат, если это не NotImplemented». 2. Если кто-то все еще хочет реализовать __ne__, более общая реализация (которая, как мне кажется, используется в Python 3): x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Приведенные реализации __eq__ и __ne__ являются субоптимальными: if isinstance(other, type(self)): дает 22 вызова __eq__ и 10 вызовов __ne__, тогда как if isinstance(self, type(other)): дает 16 вызовов __eq__ и 6 вызовов __ne__.
Хотя этот ответ в конечном итоге достигает правильной реализации, он попадает туда очень запутанным образом. Он неоднократно показывает неправильный код таким образом, что предлагает читателям подумать, что их проблемы решены, и перестать читать, и он говорит, что другие ответы не работают, а затем запускает описание проблем, которые обрабатывают другие ответы делать, прежде чем переходить к частям, которые они не используют т. Кроме того, здесь не упоминаются случаи, когда вы должны установить __hash__ на None вместо того, чтобы реализовывать его.
Проверка isinstance (other, ...) была очень полезна при работе с проверками на None. Спасибо!
Он спросил об элегантности, но он стал крепким.
n1 == n3 тоже должен быть True даже для классического класса? Потому что в этом случае other должен быть n3, а isinstance(n3, Number) - True?
Это не отвечает на вопрос.
Из этого ответа: https://stackoverflow.com/a/30676267/541136 Я продемонстрировал это, хотя правильно определять __ne__ в терминах __eq__ - вместо
def __ne__(self, other):
return not self.__eq__(other)
вы должны использовать:
def __ne__(self, other):
return not self == other
Вместо использования подклассов / миксинов я предпочитаю использовать декоратор общего класса.
def comparable(cls):
""" Class decorator providing generic comparison functionality """
def __eq__(self, other):
return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
def __ne__(self, other):
return not self.__eq__(other)
cls.__eq__ = __eq__
cls.__ne__ = __ne__
return cls
Применение:
@comparable
class Number(object):
def __init__(self, x):
self.x = x
a = Number(1)
b = Number(1)
assert a == b
Это включает комментарии к ответу Алгориаса и сравнивает объекты по одному атрибуту, потому что меня не волнует весь dict. hasattr(other, "id") должен быть истинным, но я знаю, что это потому, что я установил его в конструкторе.
def __eq__(self, other):
if other is self:
return True
if type(other) is not type(self):
# delegate to superclass
return NotImplemented
return other.id == self.id
+1, поскольку я не знал, что dict использует пословное равенство для ==, я предположил, что он считает их равными только для тех же объектных dicts. Я думаю, это очевидно, поскольку в Python есть оператор
is, позволяющий отличать идентичность объекта от сравнения значений.