Python, я должен реализовать оператор __ne__() на основе __eq__?

У меня есть класс, где я хочу переопределить __eq__() оператор. Кажется, имеет смысл переопределить __ne__() оператор, но имеет ли смысл реализовывать __ne__ основанный на __eq__ в качестве таких?

class A:
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self.__eq__(other)

Или я что-то упускаю из-за того, что Python использует эти операторы, что делает это не очень хорошей идеей?

5 ответов

Решение

Да, это прекрасно. На самом деле, документация призывает вас определить __ne__ когда вы определяете __eq__:

Не существует никаких подразумеваемых отношений между операторами сравнения. Правда о x==y не означает, что x!=y ложно Соответственно при определении __eq__()следует также определить __ne__() так что операторы будут вести себя как положено.

Во многих случаях (таких как этот) это будет так же просто, как отрицание результата __eq__, но не всегда.

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 гласит:

Не существует никаких подразумеваемых отношений между операторами сравнения. Правда о 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, Числовые методы и методы расширенного сравнения могут возвращать это значение, если они не реализуют операцию для предоставленных операндов. (Затем интерпретатор попытается выполнить отраженную операцию или какой-либо другой запасной вариант, в зависимости от оператора.) Его истинное значение равно true.

Если дан богатый оператор сравнения, если они не одного типа, 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)

Ожидаемое поведение:

(Примечание: хотя каждое второе утверждение каждого из приведенных ниже эквивалентно и, следовательно, логически избыточно тому, что было до него, я включаю их, чтобы продемонстрировать, что порядок не имеет значения, когда один является подклассом другого.)

Эти случаи имеют __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 (self->ob_type->tp_richcompare == NULL) {
            res = Py_NotImplemented;
            Py_INCREF(res);
            break;
        }
        res = (*self->ob_type->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);
            }
        }

Здесь мы видим

Но по умолчанию __ne__ использования __eq__?

Python 3 по умолчанию __ne__ детали реализации на уровне 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.

Заключение

Для Python 2-совместимого кода используйте == реализовать __ne__, Это больше:

  • правильный
  • просто
  • производительный

Только в Python 3 используйте низкоуровневое отрицание на уровне C - оно еще более простое и производительное (хотя программист отвечает за определение его правильности).

Не пишите низкоуровневую логику в высокоуровневом Python.

Просто для записи, канонически правильный и кросс Py2/Py3 портативный __ne__ будет выглядеть так:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

Это работает с любым __eq__ вы можете определить, и в отличие от not (self == other), не мешает в некоторых раздражающих / сложных случаях, связанных со сравнениями между экземплярами, где один экземпляр является подклассом другого. Если твой __eq__ не использует NotImplemented возвращается, это работает (с бессмысленными издержками), если он использует NotImplemented иногда это обрабатывает это должным образом. А проверка версии Python означает, что если класс importв Python 3, __ne__ не определено, что позволяет использовать эффективный запасной вариант Python __ne__ реализация (версия C выше), чтобы взять на себя.

Краткий ответ: да (но, чтобы сделать это правильно, прочитайте документацию)

Хотя интересный ответ Аарона Холла не является правильным способом реализации __ne__ метод, потому что с not self == other реализация, __ne__ метод другого операнда никогда не рассматривается. В отличие от этого, как показано ниже, стандартная реализация Python 3 __ne__ метод операнда делает запасной вариант на __ne__ метод другого операнда, возвращая NotImplemented когда это __eq__ метод возвращает NotImplemented, ShadowRanger дал правильную реализацию __ne__ метод:

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

Реализация операторов сравнения

Справочник по языку Python для Python 3 гласит в главе III "Модель данных":

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

Это так называемые методы "богатого сравнения". Соответствие между символами оператора и именами методов выглядит следующим образом: x<y звонки x.__lt__(y), x<=y звонки x.__le__(y), x==y звонки x.__eq__(y), x!=y звонки x.__ne__(y), x>y звонки x.__gt__(y), а также x>=y звонки x.__ge__(y),

Богатый метод сравнения может вернуть синглтон NotImplemented если он не реализует операцию для данной пары аргументов.

Не существует версий этих методов со свопированными аргументами (которые будут использоваться, когда левый аргумент не поддерживает операцию, но правый аргумент поддерживает); скорее, __lt__() а также __gt__() являются отражением друг друга, __le__() а также __ge__() являются отражением друг друга, и __eq__() а также __ne__() являются их собственным отражением. Если операнды имеют разные типы, а тип правого операнда является прямым или косвенным подклассом типа левого операнда, отраженный метод правого операнда имеет приоритет, в противном случае метод левого операнда имеет приоритет. Виртуальные подклассы не рассматриваются.

Переводя это в код Python (operator_eq за ==, operator_ne за !=, operator_lt за <, operator_gt за >, operator_le за <= а также operator_ge за >=):

def operator_eq(left, right):
    if isinstance(right, type(left)):
        result = right.__eq__(left)

        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)

        if result is NotImplemented:
            result = right.__eq__(left)

    if result is NotImplemented:
        result = left is right

    return result


def operator_ne(left, right):
    if isinstance(right, type(left)):
        result = right.__ne__(left)

        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)

        if result is NotImplemented:
            result = right.__ne__(left)

    if result is NotImplemented:
        result = left is not right

    return result


def operator_lt(left, right):
    if isinstance(right, type(left)):
        result = right.__gt__(left)

        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)

        if result is NotImplemented:
            result = right.__gt__(left)

    if result is NotImplemented:
        raise TypeError(f"'<' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_gt(left, right):
    if isinstance(right, type(left)):
        result = right.__lt__(left)

        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)

        if result is NotImplemented:
            result = right.__lt__(left)

    if result is NotImplemented:
        raise TypeError(f"'>' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_le(left, right):
    if isinstance(right, type(left)):
        result = right.__ge__(left)

        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)

        if result is NotImplemented:
            result = right.__ge__(left)

    if result is NotImplemented:
        raise TypeError(f"'<=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_ge(left, right):
    if isinstance(right, type(left)):
        result = right.__le__(left)

        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)

        if result is NotImplemented:
            result = right.__le__(left)

    if result is NotImplemented:
        raise TypeError(f"'>=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result

Реализация методов сравнения по умолчанию

Документация добавляет:

По умолчанию, __ne__() делегаты __eq__() и инвертирует результат, если это не NotImplemented, Других подразумеваемых отношений между операторами сравнения нет, например, правда (x<y or x==y) не подразумевает x<=y,

Реализация по умолчанию методов сравнения (__eq__, __ne__, __lt__, __gt__, __le__ а также __ge__) таким образом можно дать:

def __eq__(self, other):
    return NotImplemented

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

def __lt__(self, other):
    return NotImplemented

def __gt__(self, other):
    return NotImplemented

def __le__(self, other):
    return NotImplemented

def __ge__(self, other):
    return NotImplemented

Так что это правильная реализация __ne__ метод. И это не всегда возвращает обратное __eq__ метод, потому что, когда __eq__ метод возвращает NotImplemented, его обратное not NotImplemented является False (как bool(NotImplemented) является True) вместо желаемого NotImplemented,

Неправильные реализации __ne__

Как показал Аарон Холл, not self.__eq__(other) не является правильной реализацией __ne__ метод. Но и не not self == other , Последнее продемонстрировано ниже, сравнивая поведение реализации по умолчанию с поведением not self == other реализация в двух случаях:

  • __eq__ метод возвращает NotImplemented;
  • __eq__ метод возвращает значение, отличное от NotImplemented,

Реализация по умолчанию

Посмотрим, что произойдет, когда A.__ne__ Метод использует реализацию по умолчанию и A.__eq__ метод возвращает NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. != звонки A.__ne__,
  2. A.__ne__ звонки A.__eq__,
  3. A.__eq__ возвращается NotImplemented,
  4. != звонки B.__ne__,
  5. B.__ne__ возвращается "B.__ne__",

Это показывает, что когда A.__eq__ метод возвращает NotImplemented, A.__ne__ метод прибегает к B.__ne__ метод.

Теперь давайте посмотрим, что происходит, когда A.__ne__ Метод использует реализацию по умолчанию и A.__eq__ метод возвращает значение, отличное от NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. != звонки A.__ne__,
  2. A.__ne__ звонки A.__eq__,
  3. A.__eq__ возвращается True,
  4. != возвращается not True, то есть False,

Это показывает, что в этом случае A.__ne__ метод возвращает инверсию A.__eq__ метод. Таким образом __ne__ Метод ведет себя как рекламируется в документации.

Переопределение реализации по умолчанию A.__ne__ метод с правильной реализацией, приведенной выше, дает те же результаты.

not self == other реализация

Давайте посмотрим, что происходит при переопределении реализации по умолчанию A.__ne__ метод с not self == other реализация и A.__eq__ метод возвращает NotImplemented:

class A:

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is True
  1. != звонки A.__ne__,
  2. A.__ne__ звонки ==,
  3. == звонки A.__eq__,
  4. A.__eq__ возвращается NotImplemented,
  5. == звонки B.__eq__,
  6. B.__eq__ возвращается NotImplemented,
  7. == возвращается A() is B(), то есть False,
  8. A.__ne__ возвращается not False, то есть True,

Реализация по умолчанию __ne__ метод вернулся "B.__ne__" не True,

Теперь давайте посмотрим, что происходит при переопределении реализации по умолчанию A.__ne__ метод с not self == other реализация и A.__eq__ метод возвращает значение, отличное от NotImplemented:

class A:

    def __eq__(self, other):
        return True

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. != звонки A.__ne__,
  2. A.__ne__ звонки ==,
  3. == звонки A.__eq__,
  4. A.__eq__ возвращается True,
  5. A.__ne__ возвращается not True, то есть False,

Реализация по умолчанию __ne__ метод также вернулся False в этом случае.

Поскольку эта реализация не в состоянии воспроизвести поведение реализации по умолчанию __ne__ метод, когда __eq__ метод возвращает NotImplemented Неверно.

Если все __eq__, __ne__, __lt__, __ge__, __le__, а также __gt__ имеет смысл для класса, а затем просто реализовать __cmp__ вместо. В противном случае, делайте так, как делаете, из-за того, что сказал Даниэль ДиПаоло (пока я тестировал его, а не искал;))

Другие вопросы по тегам