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 говорится:
Между операторами сравнения нет подразумеваемых отношений. Истина 18_ не означает, что x!=y
ложно. Соответственно, при определении __eq__()
следует также определить __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)
Ожидаемое поведение:
(Примечание: хотя каждое второе утверждение каждого из приведенных ниже утверждений эквивалентно и, следовательно, логически избыточно по отношению к предыдущему, я включаю их, чтобы продемонстрировать, что порядок не имеет значения, если одно является подклассом другого. em>)
Эти экземпляры __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 (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 # <- this boolean changed...
>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)
Вывод
Для кода, совместимого с Python 2, используйте ==
для реализации __ne__
. Это больше:
- правильный
- просто
- исполнитель
Только в Python 3 используйте низкоуровневое отрицание на уровне C - это даже более проще и производительнее (хотя программист несет ответственность за определение, что это правильно) .
Опять же, не пишите низкоуровневую логику на высокоуровневом Python.
person
Aaron Hall
schedule
05.06.2015