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 использует эти операторы, что делает это не очень хорошей идеей?
5 ответов
Да, это прекрасно. На самом деле, документация призывает вас определить __ne__
когда вы определяете __eq__
:
Не существует никаких подразумеваемых отношений между операторами сравнения. Правда о
x==y
не означает, чтоx!=y
ложно Соответственно при определении__eq__()
следует также определить__ne__()
так что операторы будут вести себя как положено.
Во многих случаях (таких как этот) это будет так же просто, как отрицание результата __eq__
, но не всегда.
Python, я должен реализовать
__ne__()
оператор на основе__eq__
?
Краткий ответ: Нет. Использование ==
вместо __eq__
В Python 3 !=
это отрицание ==
по умолчанию, так что вам даже не нужно писать __ne__
и документация больше не учитывается при ее написании.
Вообще говоря, для кода только для Python 3 не пишите его, если вам не нужно затмевать родительскую реализацию, например, для встроенного объекта.
То есть имейте в виду комментарий Раймона Хеттингера:
__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__
с точки зрения обратного __eq__
мы можем получить последовательное поведение.
Этот раздел документации был обновлен для Python 3:
По умолчанию,
__ne__()
делегаты__eq__()
и инвертирует результат, если это неNotImplemented
,
и в разделе "что нового" мы видим, что это поведение изменилось:
!=
теперь возвращает противоположность==
если==
возвращаетсяNotImplemented
,
Для реализации __ne__
мы предпочитаем использовать ==
оператор вместо использования __eq__
метод напрямую, так что если self.__eq__(other)
из подкласса возвращается NotImplemented
для проверенного типа Python соответствующим образом проверит other.__eq__(self)
Из документации:
NotImplemented
объект
Этот тип имеет одно значение. Существует один объект с этим значением. Этот объект доступен через встроенное имя
NotImplemented
, Числовые методы и методы расширенного сравнения могут возвращать это значение, если они не реализуют операцию для предоставленных операндов. (Затем интерпретатор попытается выполнить отраженную операцию или какой-либо другой запасной вариант, в зависимости от оператора.) Его истинное значение равно true.
Если дан богатый оператор сравнения, если они не одного типа, 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.
Просто для записи, канонически правильный и кросс 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
в Python 3, __ne__
не определено, что позволяет использовать эффективный запасной вариант Python __ne__
реализация (версия C выше), чтобы взять на себя.
Краткий ответ: да (но, чтобы сделать это правильно, прочитайте документацию)
Хотя интересный ответ Аарона Холла не является правильным способом реализации __ne__
метод, потому что с not self == other
реализация, __ne__
метод другого операнда никогда не рассматривается. В отличие от этого, как показано ниже, стандартная реализация Python 3 __ne__
метод операнда делает запасной вариант на __ne__
метод другого операнда, возвращая NotImplemented
когда это __eq__
метод возвращает NotImplemented
, ShadowRanger дал правильную реализацию __ne__
метод:
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
return not result
return NotImplemented
Реализация операторов сравнения
Справочник по языку 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 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 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 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 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 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 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__
метод. Но и не 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
реализация и 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
реализация и 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
Неверно.
Если все __eq__
, __ne__
, __lt__
, __ge__
, __le__
, а также __gt__
имеет смысл для класса, а затем просто реализовать __cmp__
вместо. В противном случае, делайте так, как делаете, из-за того, что сказал Даниэль ДиПаоло (пока я тестировал его, а не искал;))