Python, чи повинен я реалізувати __ne__()
оператор на основі __eq__
?
Коротка відповідь: Не впроваджуйте це, але якщо потрібно, використовуйте ==
, ні__eq__
У Python 3 !=
це заперечення ==
за замовчуванням, тому вам навіть не потрібно писати a __ne__
, і документація більше не має думки щодо його написання.
Взагалі кажучи, для коду лише для Python 3 не пишіть його, якщо вам не потрібно затьмарити батьківську реалізацію, наприклад, для вбудованого об'єкта.
Тобто майте на увазі коментар Реймонда Хеттінгера :
__ne__
Метод автоматично випливає з __eq__
тільки , якщо
__ne__
ще не визначена в суперкласу. Отже, якщо ви успадковуєте вбудовану версію, найкраще замінити обидва.
Якщо вам потрібен ваш код для роботи в Python 2, дотримуйтесь рекомендацій для Python 2, і він буде добре працювати в Python 3.
У Python 2, сам Python автоматично не реалізує жодну операцію в іншому, тому слід визначити __ne__
терміни ==
замість __eq__
. EG
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
. Числові методи та розширені методи порівняння можуть повернути це значення, якщо вони не реалізують операцію для наданих операндів. (Потім інтерпретатор спробує відображену операцію або будь-який інший резервний варіант, залежно від оператора.) Її істинність є істинною.
Коли дається багатий оператор порівняння, якщо вони не той же самий тип, 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 # 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
Це необхідно для коректності для API рівня Python на рівні 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 високого рівня.
__ne__
використовувати__eq__
, лише те, що ви її реалізуєте.