Python, следует ли реализовать __ne__()
оператор на основе __eq__
?
Краткий ответ: не внедряйте, но если нужно, используйте ==
, а не__eq__
В Python 3 по умолчанию используется !=
отрицание ==
, поэтому от вас даже не требуется писать a __ne__
, и документация больше не требует его написания.
Вообще говоря, для кода, предназначенного только для Python 3, не пишите его, если вам не нужно перекрывать родительскую реализацию, например, для встроенного объекта.
То есть имейте в виду комментарий Раймона Хеттингера :
__ne__
Метод автоматически следует из __eq__
только , если
__ne__
еще не определена в суперкласса. Итак, если вы наследуете от встроенного, лучше переопределить оба.
Если вам нужно, чтобы ваш код работал на Python 2, следуйте рекомендациям для Python 2, и он будет отлично работать на Python 3.
В Python 2 сам Python не реализует автоматически какую-либо операцию в терминах другой, поэтому вам следует определять __ne__
in в терминах ==
вместо __eq__
. НАПРИМЕР
class A(object):
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self == other
Смотрите доказательство того, что
__ne__()
оператор реализации на основе __eq__
и
- вообще не реализуется
__ne__
в Python 2
обеспечивает некорректное поведение в демонстрации ниже.
Длинный ответ
В документации для Python 2 говорится:
Между операторами сравнения нет подразумеваемых отношений. Истина x==y
не означает, что x!=y
это ложь. Соответственно, при определении __eq__()
следует также определить, __ne__()
чтобы операторы вели себя так, как ожидалось.
Это означает, что если мы определим __ne__
через обратное __eq__
, мы можем добиться согласованного поведения.
Этот раздел документации был обновлен для Python 3:
По умолчанию __ne__()
делегирует __eq__()
и инвертирует результат, если это не так NotImplemented
.
а в разделе «Что нового» мы видим, что поведение изменилось:
!=
now возвращает обратное ==
, если не ==
возвращает NotImplemented
.
Для реализации __ne__
мы предпочитаем использовать ==
оператор вместо использования __eq__
метода напрямую, чтобы, если self.__eq__(other)
подкласс возвращает NotImplemented
проверенный тип, Python соответствующим образом проверит other.__eq__(self)
Из документации :
NotImplemented
объект
Этот тип имеет единственное значение. Это единственный объект с этим значением. Доступ к этому объекту осуществляется через встроенное имя
NotImplemented
. Числовые методы и методы расширенного сравнения могут возвращать это значение, если они не реализуют операцию для предоставленных операндов. (Интерпретатор затем попробует отраженную операцию или другой откат, в зависимости от оператора.) Его истинное значение истинно.
Когда дается богатый оператор сравнения, если они не тот же самый тип, Python проверяет , является ли other
это подтип, и если у него есть , что оператор , определенный, он использует other
первый метод «s (обратный для <
, <=
, >=
и >
). Если 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
assert not wrong2 == wrong1
Неожиданное поведение:
Обратите внимание, что это сравнение противоречит приведенным выше сравнениям ( 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
True
Результат должен быть выше False
!
Исходный код Python 3
Реализация CPython по умолчанию для __ne__
находится typeobject.c
вobject_richcompare
:
case Py_NE:
if (Py_TYPE(self)->tp_richcompare == NULL) {
res = Py_NotImplemented;
Py_INCREF(res);
break;
}
res = (*Py_TYPE(self)->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);
}
}
break;
Но по умолчанию __ne__
использует __eq__
?
Детали__ne__
реализации Python 3 по умолчанию на уровне 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.
Ответ некоторым критикам
Другой отвечающий пишет:
Реализация Аарона Холл not self == other
из __ne__
метода некорректна , так как он никогда не сможет вернуться NotImplemented
( not NotImplemented
в False
) и , следовательно, __ne__
метод , который имеет приоритет никогда не может упасть обратно на __ne__
методе , который не имеет приоритета.
То, что вы __ne__
никогда не вернетесь NotImplemented
, не делает его неправильным. Вместо этого мы обрабатываем приоритизацию с NotImplemented
помощью проверки на равенство с ==
. Предполагая ==
, что все выполнено правильно, мы закончили.
not self == other
Раньше __ne__
это была реализация метода по умолчанию в Python 3, но это была ошибка, и она была исправлена в Python 3.4 в январе 2015 года, как заметил ShadowRanger (см. проблему № 21408).
Что ж, давайте это объясним.
Как отмечалось ранее, Python 3 по умолчанию обрабатывает __ne__
, сначала проверяя, self.__eq__(other)
возвращает ли он NotImplemented
(синглтон), что следует проверить с помощью is
и вернуть, если да, иначе он должен вернуть обратное. Вот эта логика, написанная как миксин классов:
class CStyle__ne__:
"""Mixin that provides __ne__ functionality equivalent to
the builtin functionality
"""
def __ne__(self, other):
equal = self.__eq__(other)
if equal is NotImplemented:
return NotImplemented
return not equal
Это необходимо для корректности Python API уровня C, и это было введено в Python 3, что делает
избыточный. Все соответствующие __ne__
методы были удалены, в том числе те, которые реализуют собственную проверку, а также те, которые делегируют полномочия __eq__
напрямую или через ==
- и это ==
был наиболее распространенный способ сделать это.
Важна ли симметрия?
Наш настойчивый критик оказывает патологический пример , чтобы сделать дело для обработки NotImplemented
в __ne__
, оценке симметрии выше всего остального. Давайте проиллюстрируем аргумент ясным примером:
class B:
"""
this class has no __eq__ implementation, but asserts
any instance is not equal to any other object
"""
def __ne__(self, other):
return True
class A:
"This class asserts instances are equivalent to all other objects"
def __eq__(self, other):
return True
>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)
Итак, по этой логике, чтобы сохранить симметрию, нам нужно написать сложное __ne__
, независимо от версии Python.
class B:
def __ne__(self, other):
return True
class A:
def __eq__(self, other):
return True
def __ne__(self, other):
result = other.__eq__(self)
if result is NotImplemented:
return NotImplemented
return not result
>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)
Очевидно, нам не следует думать, что эти случаи равны и не равны.
Я полагаю, что симметрия менее важна, чем презумпция разумного кода и следование советам документации.
Однако, если бы у A была разумная реализация __eq__
, мы все равно могли бы следовать моему направлению здесь, и у нас все еще была бы симметрия:
class B:
def __ne__(self, other):
return True
class A:
def __eq__(self, other):
return False
>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)
Вывод
Для кода, совместимого с Python 2, используйте ==
для реализации __ne__
. Это больше:
- верный
- просто
- исполнитель
Только в Python 3 используйте низкоуровневое отрицание на уровне C - оно еще более простое и производительное (хотя программист несет ответственность за определение его правильности ).
Опять же, не пишите логику низкого уровня на Python высокого уровня.
__ne__
using__eq__
, а только ее реализации.