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__, лише те, що ви її реалізуєте.