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__, а только ее реализации.