Python, должен ли я реализовать __ne __() оператор на основе __eq__?
У меня есть класс, где я хочу переопределить оператор __eq__()
. Кажется, что я должен переопределить оператор __ne__()
, но имеет ли смысл реализовать __ne__
на основе __eq__
как такового?
class A:
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
Или есть что-то, что мне не хватает в том, как Python использует эти операторы, что делает это не очень хорошая идея?
Ответы
Ответ 1
Да, это прекрасно. Фактически документация настоятельно рекомендует определить __ne__
при определении __eq__
:
Нет подразумеваемых отношений среди операторов сравнения. истина x==y
не означает, что x!=y
false. Соответственно, при определении __eq__()
, следует также определить __ne__()
, чтобы операторы действовали должным образом.
Во многих случаях (например, этот) это будет так же просто, как отрицание результата __eq__
, но не всегда.
Ответ 2
Python, должен ли я реализовать __ne__()
оператор на основе __eq__
?
Короткий ответ: Нет. Используйте ==
вместо __eq__
В Python 3 !=
Является отрицанием ==
по умолчанию, поэтому вам даже не требуется писать __ne__
, и документация больше не ущемляется при написании.
Вообще говоря, для кода Python 3-only не пишите, если вам не нужно заслонять родительскую реализацию, например, для встроенного объекта.
То есть, имейте в виду Raymond Hettinger comment:
Метод __ne__
автоматически следует из __eq__
только если __ne__
еще не определен в суперклассе. Итак, если вы наследуете от встроенного, лучше всего переопределить оба.
Если вам нужен ваш код для работы в Python 2, следуйте рекомендациям для Python 2, и он отлично работает в Python 3.
В Python 2 сам Python автоматически не выполняет какую-либо операцию в терминах другого - поэтому вы должны определить __ne__
в терминах ==
вместо __eq__
. НАПРИМЕР
class A(object):
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self == other # NOT 'return not self.__eq__(other)'
Смотрите доказательство того, что
- реализуя
__ne__()
на основе __eq__
и - не реализует
__ne__
в Python 2 вообще
обеспечивает неправильное поведение на демонстрации ниже.
Длительный ответ
В документации для Python 2 говорится:
Между операторами сравнения нет подразумеваемых отношений. Истина x==y
не означает, что x!=y
ложно. Соответственно, при определении __eq__()
следует также определить __ne__()
чтобы операторы __ne__()
образом.
Таким образом, это означает, что если мы определим __ne__
в терминах инверсии __eq__
, мы получим согласованное поведение.
Этот раздел документации был обновлен для Python 3:
По умолчанию __ne__()
делегирует __eq__()
и инвертирует результат, если не является NotImplemented
.
и в разделе "что нового" мы видим, что это поведение изменилось:
-
!=
теперь возвращает противоположность ==
, если ==
возвращает NotImplemented
.
Для реализации __ne__
, мы предпочитаем использовать ==
оператор вместо использования __eq__
метода непосредственно, так что если self.__eq__(other)
подкласс возвращает NotImplemented
для типа проверяемого, Python будет надлежащим образом проверить other.__eq__(self)
Из документация:
Объект NotImplemented
Этот тип имеет одно значение. Существует один объект с этим значением. Доступ к этому объекту осуществляется через встроенное имя NotImplemented
. Числовые методы и богатые методы сравнения могут вернуть это значение, если они не реализуют операцию для предоставленных операндов. (Интерпретатор затем попробует отраженную операцию или какой-либо другой резерв, в зависимости от оператора.) Его истинное значение истинно.
Когда задан богатый оператор сравнения, если он не является одним и тем же типом, Python проверяет, является ли other
подтипом, и если он имеет этот оператор, он сначала использует other
метод (обратный для <
, <=
, >=
и >
). Если NotImplemented
возвращается, то он использует противоположный метод. (Он не проверяет один и тот же метод дважды.) Использование оператора ==
допускает эту логику.
ожидания
Семантически, вы должны реализовать __ne__
в терминах проверки равенства, потому что пользователи вашего класса ожидают, что следующие функции будут эквивалентны для всех экземпляров A.:
def negation_of_equals(inst1, inst2):
"""always should return same as not_equals(inst1, inst2)"""
return not inst1 == inst2
def not_equals(inst1, inst2):
"""always should return same as negation_of_equals(inst1, inst2)"""
return inst1 != inst2
То есть обе указанные функции всегда должны возвращать один и тот же результат. Но это зависит от программиста.
Демонстрация неожиданного поведения при определении __ne__
на основе __eq__
:
Сначала установите:
class BaseEquatable(object):
def __init__(self, x):
self.x = x
def __eq__(self, other):
return isinstance(other, BaseEquatable) and self.x == other.x
class ComparableWrong(BaseEquatable):
def __ne__(self, other):
return not self.__eq__(other)
class ComparableRight(BaseEquatable):
def __ne__(self, other):
return not self == other
class EqMixin(object):
def __eq__(self, other):
"""override Base __eq__ & bounce to other for __eq__, e.g.
if issubclass(type(self), type(other)): # True in this example
"""
return NotImplemented
class ChildComparableWrong(EqMixin, ComparableWrong):
"""__ne__ the wrong way (__eq__ directly)"""
class ChildComparableRight(EqMixin, ComparableRight):
"""__ne__ the right way (uses ==)"""
class ChildComparablePy3(EqMixin, BaseEquatable):
"""No __ne__, only right in Python 3."""
Выполнять неэквивалентные экземпляры:
right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Ожидаемое поведение:
(Примечание: в то время как каждое второе утверждение каждого из ниже эквивалентно и поэтому логически избыточно относится к тому, которое было до него, я включаю их, чтобы продемонстрировать, что порядок не имеет значения, если он является подклассом другого.)
Эти экземпляры имеют __ne__
реализованные с помощью ==
:
assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1
Эти экземпляры, тестируемые под Python 3, также работают правильно:
assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1
И помните, что у них есть __ne__
реализованный с __eq__
- пока это ожидаемое поведение, реализация неверна:
assert not wrong1 == wrong2 # These are contradicted by the
assert not wrong2 == wrong1 # below unexpected behavior!
Неожиданное поведение:
Обратите внимание, что это сравнение противоречит вышеприведенным сравнениям (not wrong1 == wrong2
).
>>> assert wrong1 != wrong2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
а также,
>>> assert wrong2 != wrong1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Не пропустите __ne__
в Python 2
Для доказательства того, что вы не должны пропускать реализацию __ne__
в Python 2, см. Эти эквивалентные объекты:
>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True
Вышеприведенный результат должен быть False
!
Источник Python 3
Реализация CPython по умолчанию для __ne__
находится в typeobject.c
в object_richcompare
:
case Py_NE:
/* By default, __ne__() delegates to __eq__() and inverts the result,
unless the latter returns NotImplemented. */
if (self->ob_type->tp_richcompare == NULL) {
res = Py_NotImplemented;
Py_INCREF(res);
break;
}
res = (*self->ob_type->tp_richcompare)(self, other, Py_EQ);
if (res != NULL && res != Py_NotImplemented) {
int ok = PyObject_IsTrue(res);
Py_DECREF(res);
if (ok < 0)
res = NULL;
else {
if (ok)
res = Py_False;
else
res = Py_True;
Py_INCREF(res);
}
}
Здесь мы видим
Но по умолчанию __ne__
использует __eq__
?
Python 3 по умолчанию __ne__
детализации реализации на уровне C использует __eq__
потому что более высокий уровень ==
(PyObject_RichCompare) будет менее эффективным - и поэтому он должен также обрабатывать NotImplemented
.
Если __eq__
правильно реализована, то отрицание ==
также правильное - и это позволяет нам избежать деталей реализации на низком уровне в нашем __ne__
.
Использование ==
позволяет нам сохранять нашу логику низкого уровня в одном месте и избегать обращения к NotImplemented
в __ne__
.
Можно ошибочно предположить, что ==
может возвращать NotImplemented
.
Он фактически использует ту же логику, что и стандартная реализация __eq__
, которая проверяет личность (см. Do_richcompare и наши доказательства ниже)
class Foo:
def __ne__(self, other):
return NotImplemented
__eq__ = __ne__
f = Foo()
f2 = Foo()
И сравнения:
>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True
Спектакль
Не верьте мне на слово, давайте посмотрим, что еще лучше:
class CLevel:
"Use default logic programmed in C"
class HighLevelPython:
def __ne__(self, other):
return not self == other
class LowLevelPython:
def __ne__(self, other):
equal = self.__eq__(other)
if equal is NotImplemented:
return NotImplemented
return not equal
def c_level():
cl = CLevel()
return lambda: cl != cl
def high_level_python():
hlp = HighLevelPython()
return lambda: hlp != hlp
def low_level_python():
llp = LowLevelPython()
return lambda: llp != llp
Я думаю, что эти показатели производительности говорят сами за себя:
>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029
Это имеет смысл, если вы считаете, что low_level_python
выполняет логику в Python, которая иначе обрабатывалась бы на уровне C.
Заключение
Для совместимого с Python 2 кода используйте ==
для реализации __ne__
. Это больше:
- правильный
- просто
- производительный
В Python 3 используйте отрицание низкого уровня на уровне C - он еще более прост и эффективен (хотя программист отвечает за определение того, что он правильный).
Не пишите низкоуровневую логику на Python высокого уровня.
Ответ 3
Только для записи канонически правильная и перекрестная портативная версия Py2/Py3 __ne__
будет выглядеть так:
import sys
class ...:
...
def __eq__(self, other):
...
if sys.version_info[0] == 2:
def __ne__(self, other):
equal = self.__eq__(other)
return equal if equal is NotImplemented else not equal
Это работает с любым __eq__
, который вы могли бы определить, и в отличие от not (self == other)
не вмешивается в некоторые досадные/сложные случаи, связанные с сравнениями между экземплярами, где один экземпляр является подклассом другого. Если ваш __eq__
не использует NotImplemented
возвращает, это работает (с бессмысленными служебными данными), если он иногда использует NotImplemented
, это правильно обрабатывает его. И проверка версии Python означает, что если класс import
-ed в Python 3, __ne__
оставлен undefined, что позволяет нативный Python, эффективный резерв __ne__
(версия C выше), чтобы взять верх.
Ответ 4
Краткий ответ: да (но, чтобы сделать это правильно, прочитайте документацию)
Реализация метода __ne__
в __ne__
является правильной (в том смысле, что она ведет себя точно так же, как и реализация Python 3 по умолчанию):
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
return not result
return NotImplemented
Реализация Aaron Halls, __ne__
от not self == other
метода __ne__
неверна, поскольку он никогда не может NotImplemented
(not NotImplemented
is False
), и, следовательно, метод __ne__
, имеющий приоритет, никогда не может __ne__
метод __ne__
, у которого нет приоритета. not self == other
раньше был реализацией метода __ne__
по умолчанию в Python 3, но это была ошибка, и она была исправлена в Python 3.4 в январе 2015 года, как заметил ShadowRanger (см. проблему # 21408).
Реализация операторов сравнения
Справочник по языку Python для Python 3 гласит в главе III "Модель данных":
object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)
Это так называемые методы "богатого сравнения". Соответствие между символами операторов и именами методов выглядит следующим образом: x<y
вызывает x.__lt__(y)
, x<=y
вызывает x.__le__(y)
, x==y
вызывает x.__eq__(y)
, x!=y
вызывает x.__ne__(y)
, x>y
вызывает x.__gt__(y)
, а x>=y
вызывает x.__ge__(y)
.
Метод расширенного сравнения может возвращать одноэлементный NotImplemented
если он не реализует операцию для данной пары аргументов.
Не существует версий этих методов со свопированными аргументами (которые будут использоваться, когда левый аргумент не поддерживает операцию, но правый аргумент поддерживает); скорее, __lt__()
и __gt__()
являются отражением друг друга, __le__()
и __ge__()
являются отражением друг друга, а __eq__()
и __ne__()
являются их собственным отражением. Если операнды имеют разные типы, и тип правого операнда является прямым или косвенным подклассом типа левого операнда, отраженный метод правого операнда имеет приоритет, в противном случае метод левого операнда имеет приоритет. Виртуальные подклассы не рассматриваются.
Перевод этого в код Python дает (используя operator_eq
для ==
, operator_ne
для !=
, operator_lt
для <
, operator_gt
для >
, operator_le
для <=
и operator_ge
для >=
):
def operator_eq(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__eq__(left)
if result is NotImplemented:
result = left.__eq__(right)
else:
result = left.__eq__(right)
if result is NotImplemented:
result = right.__eq__(left)
if result is NotImplemented:
result = left is right
return result
def operator_ne(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__ne__(left)
if result is NotImplemented:
result = left.__ne__(right)
else:
result = left.__ne__(right)
if result is NotImplemented:
result = right.__ne__(left)
if result is NotImplemented:
result = left is not right
return result
def operator_lt(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__gt__(left)
if result is NotImplemented:
result = left.__lt__(right)
else:
result = left.__lt__(right)
if result is NotImplemented:
result = right.__gt__(left)
if result is NotImplemented:
raise TypeError(f"'<' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")
return result
def operator_gt(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__lt__(left)
if result is NotImplemented:
result = left.__gt__(right)
else:
result = left.__gt__(right)
if result is NotImplemented:
result = right.__lt__(left)
if result is NotImplemented:
raise TypeError(f"'>' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")
return result
def operator_le(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__ge__(left)
if result is NotImplemented:
result = left.__le__(right)
else:
result = left.__le__(right)
if result is NotImplemented:
result = right.__ge__(left)
if result is NotImplemented:
raise TypeError(f"'<=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")
return result
def operator_ge(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__le__(left)
if result is NotImplemented:
result = left.__ge__(right)
else:
result = left.__ge__(right)
if result is NotImplemented:
result = right.__le__(left)
if result is NotImplemented:
raise TypeError(f"'>=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")
return result
Реализация методов сравнения по умолчанию
Документация добавляет:
По умолчанию __ne__()
делегирует __eq__()
и инвертирует результат, если только он не NotImplemented
. Других подразумеваемых отношений между операторами сравнения нет, например, истинность (x<y or x==y)
не подразумевает x<=y
.
Реализация по умолчанию методов сравнения (__eq__
, __ne__
, __lt__
, __gt__
, __le__
и __ge__
) может быть определена как:
def __eq__(self, other):
return NotImplemented
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
return not result
return NotImplemented
def __lt__(self, other):
return NotImplemented
def __gt__(self, other):
return NotImplemented
def __le__(self, other):
return NotImplemented
def __ge__(self, other):
return NotImplemented
Так что это правильная реализация метода __ne__
. И он не всегда возвращает обратный метод __eq__
потому что когда метод __eq__
возвращает NotImplemented
, его обратный not NotImplemented
имеет значение False
(поскольку bool(NotImplemented)
равен True
) вместо требуемого NotImplemented
.
Неправильные реализации __ne__
Как показал Аарон Холл, not self.__eq__(other)
не является реализацией метода __ne__
по __ne__
. Но ни я not self == other
. Последнее продемонстрировано ниже, сравнивая поведение реализации по умолчанию с поведением not self == other
реализации в двух случаях:
-
__eq__
метод возвращает NotImplemented
; - метод
__eq__
возвращает значение, отличное от NotImplemented
.
Реализация по умолчанию
Давайте посмотрим, что происходит, когда метод A.__ne__
использует реализацию по умолчанию, а метод A.__eq__
возвращает NotImplemented
:
class A:
pass
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) == "B.__ne__"
-
!=
вызывает A.__ne__
. -
A.__ne__
вызывает A.__eq__
. -
A.__eq__
возвращает NotImplemented
. -
!=
звонит B.__ne__
. -
B.__ne__
возвращает "B.__ne__"
.
Это показывает, что, когда A.__eq__
метод возвращает NotImplemented
, то A.__ne__
метод падает обратно на B.__ne__
метода.
Теперь давайте посмотрим, что происходит, когда метод A.__ne__
использует реализацию по умолчанию, а метод A.__eq__
возвращает значение, отличное от NotImplemented
:
class A:
def __eq__(self, other):
return True
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) is False
-
!=
вызывает A.__ne__
. -
A.__ne__
вызывает A.__eq__
. -
A.__eq__
возвращает True
. -
!=
возвращает not True
, то есть False
.
Это показывает, что в этом случае метод A.__ne__
возвращает значение, обратное методу A.__eq__
. Таким образом, метод __ne__
ведет себя так, как объявлено в документации.
Переопределение реализации по умолчанию метода A.__ne__
с правильной реализацией, приведенной выше, дает те же результаты.
not self == other
реализация
Давайте посмотрим, что происходит при переопределении реализации по умолчанию метода A.__ne__
с not self == other
реализацией not self == other
а метод A.__eq__
возвращает NotImplemented
:
class A:
def __ne__(self, other):
return not self == other
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) is True
-
!=
вызывает A.__ne__
. -
A.__ne__
звонки ==
. -
==
вызывает A.__eq__
. -
A.__eq__
возвращает NotImplemented
. -
==
звонит B.__eq__
. -
B.__eq__
возвращает NotImplemented
. -
==
возвращает A() is B()
, то есть False
. -
A.__ne__
возвращает not False
, то есть True
.
Реализация по умолчанию метода __ne__
вернула "B.__ne__"
, а не True
.
Теперь давайте посмотрим, что происходит при переопределении реализации по умолчанию метода A.__ne__
с not self == other
реализацией not self == other
а метод A.__eq__
возвращает значение, отличное от NotImplemented
:
class A:
def __eq__(self, other):
return True
def __ne__(self, other):
return not self == other
class B:
def __ne__(self, other):
return "B.__ne__"
assert (A() != B()) is False
-
!=
вызывает A.__ne__
. -
A.__ne__
звонки ==
. -
==
вызывает A.__eq__
. -
A.__eq__
возвращает True
. -
A.__ne__
возвращает not True
, то есть False
.
Реализация по умолчанию метода __ne__
также вернула False
в этом случае.
Поскольку эта реализация не в состоянии воспроизвести поведение реализации по умолчанию в __ne__
метода, когда __eq__
метод возвращает NotImplemented
, это неверно.
Ответ 5
Если все __eq__
, __ne__
, __lt__
, __ge__
, __le__
и __gt__
имеют смысл для класса, а затем просто реализуйте __cmp__
. В противном случае сделайте так, как вы делаете, из-за того, что сказал Daniel DiPaolo (пока я тестировал его вместо того, чтобы искать его;))