Элегантные способы поддержки эквивалентности ("равенства") в классах 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__
s?
Примечание: немного уточнения - когда __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
12 ответов
Рассмотрим эту простую проблему:
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__
функционировать так же, как указано в документации:
Не существует никаких подразумеваемых отношений между операторами сравнения. Правда о
x==y
не означает, чтоx!=y
ложно Соответственно при определении__eq__()
следует также определить__ne__()
так что операторы будут вести себя как положено.
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 это больше не требуется, поскольку в документации говорится:
По умолчанию,
__ne__()
делегаты__eq__()
и инвертирует результат, если это неNotImplemented
, Других подразумеваемых отношений между операторами сравнения нет, например, правда(x<y or x==y)
не подразумеваетx<=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
значение как:
Числовые методы и методы расширенного сравнения могут возвращать это значение, если они не реализуют операцию для предоставленных операндов. (Затем интерпретатор попытается выполнить отраженную операцию или какой-либо другой запасной вариант, в зависимости от оператора.) Его истинное значение равно true.
В этом случае оператор делегирует операцию сравнения отраженному методу другого операнда. Документация определяет отраженные методы как:
Не существует версий этих методов со свопированными аргументами (которые будут использоваться, когда левый аргумент не поддерживает операцию, но правый аргумент поддерживает); скорее,
__lt__()
а также__gt__()
являются отражением друг друга,__le__()
а также__ge__()
являются отражением друг друга, и__eq__()
а также__ne__()
являются их собственным отражением.
Результат выглядит так:
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
Возвращение 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
Вы должны быть осторожны с наследованием:
>>> 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
Кроме того, ваш подход будет работать нормально, для этого есть специальные методы.
То, как вы описываете, это то, как я всегда это делал. Поскольку он полностью универсален, вы всегда можете разбить эту функциональность на класс 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
Не прямой ответ, но, казалось, достаточно уместным, чтобы его можно было исправить, так как иногда это спасает от многословной скуки. Вырезать прямо из документов...
functools.total_ordering (ЦБС)
Учитывая класс, определяющий один или несколько методов упорядочения сравнительного сравнения, этот декоратор класса предоставляет остальное. Это упрощает усилия по определению всех возможных операций расширенного сравнения:
Класс должен определять один из 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()))
Вам не нужно переопределять оба __eq__
а также __ne__
вы можете переопределить только __cmp__
но это повлияет на результат ==,!==, <,> и так далее.
is
тесты на предмет идентичности. Это означает is
б будет True
в случае, когда a и b содержат ссылку на один и тот же объект. В python вы всегда держите ссылку на объект в переменной, а не на фактический объект, поэтому, по существу, для того, чтобы a b было истинным, объекты в них должны быть расположены в одной и той же ячейке памяти. Как и самое главное, почему бы вам не изменить это поведение?
Изменить: я не знал __cmp__
был удален из Python 3, поэтому избегайте его.
Из этого ответа: /questions/30635972/python-ya-dolzhen-realizovat-operator-ne-na-osnove-eq/30635990#30635990 Я продемонстрировал это, хотя это правильно определить __ne__
с точки зрения __eq__
- вместо
def __ne__(self, other):
return not self.__eq__(other)
вы должны использовать:
def __ne__(self, other):
return not self == other
Я думаю, что вы ищете два термина: равенство (==) и идентичность (есть). Например:
>>> 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
Тест '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 не несут бремени всей этой дополнительной (и в большинстве случаев идентичной) логики.
Другой элегантный способ поддержки эквивалентности — использовать@dataclass
. ТвойFoo
пример тогда станет:
from dataclasses import dataclass
@dataclass
class Foo:
item: int
Вот и все! Теперь поведение следующее:
a = Foo(1)
b = Foo(1)
print(a == b) # True
c = Foo(2)
print(a == c) # False
Если вашему классу необходимо предоставить другие атрибуты экземпляра, которые не должны играть роли в эквивалентности, определите их в__post_init__
, вот так:
from dataclasses import dataclass
from random import randint
@dataclass
class Foo:
age: int
name: str
def __post_init__(self):
self.rnd = randint(1, 100000)
a = Foo(38, "Helen")
b = Foo(38, "Helen")
print(a == b) # True
print(a.rnd == b.rnd) # False, probably ;-)
Вместо того, чтобы использовать подклассы / миксины, я хотел бы использовать универсальный декоратор класса
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
Я написал собственную базу с реализацией по умолчанию
__ne__
это просто отрицает:
class HasEq(object):
"""
Mixin that provides a default implementation of ``object.__neq__`` using the subclass's implementation of ``object.__eq__``.
This overcomes Python's deficiency of ``==`` and ``!=`` not being symmetric when overloading comparison operators
(i.e. ``not x == y`` *does not* imply that ``x != y``), so whenever you implement
`object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_, it is expected that you
also implement `object.__ne__ <https://docs.python.org/2/reference/datamodel.html#object.__ne__>`_
NOTE: in Python 3+ this is no longer necessary (see https://docs.python.org/3/reference/datamodel.html#object.__ne__)
"""
def __ne__(self, other):
"""
Default implementation of ``object.__ne__(self, other)``, delegating to ``self.__eq__(self, other)``.
When overriding ``object.__eq__`` in Python, one should also override ``object.__ne__`` to ensure that
``not x == y`` is the same as ``x != y``
(see `object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_ spec)
:return: ``NotImplemented`` if ``self.__eq__(other)`` returns ``NotImplemented``, otherwise ``not self.__eq__(other)``
"""
equal = self.__eq__(other)
# the above result could be either True, False, or NotImplemented
if equal is NotImplemented:
return NotImplemented
return not equal
Если вы унаследованы от этого базового класса, вам нужно только реализовать
__eq__
и база.
Оглядываясь назад, можно было бы подумать, что лучшим подходом было бы вместо этого реализовать его как декоратор. Что-то вроде@functools.total_ordering