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 использует эти операторы, что делает это не очень хорошая идея?

Ответы

Ответ 1

Да, это прекрасно. Фактически документация настоятельно рекомендует определить __ne__ при определении __eq__:

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

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

Ответ 2

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

Короткий ответ: Нет. Используйте == вместо __eq__

В Python 3 != Является отрицанием == по умолчанию, поэтому вам даже не требуется писать __ne__, и документация больше не ущемляется при написании.

Вообще говоря, для кода Python 3-only не пишите, если вам не нужно заслонять родительскую реализацию, например, для встроенного объекта.

То есть, имейте в виду Raymond Hettinger comment:

Метод __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__() образом.

Таким образом, это означает, что если мы определим __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)

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

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

Эти экземпляры имеют __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 высокого уровня.

Ответ 3

Только для записи канонически правильная и перекрестная портативная версия 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 -ed в Python 3, __ne__ оставлен undefined, что позволяет нативный Python, эффективный резерв __ne__ (версия C выше), чтобы взять верх.

Ответ 4

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

Реализация метода __ne__ в __ne__ является правильной (в том смысле, что она ведет себя точно так же, как и реализация Python 3 по умолчанию):

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

    if result is not NotImplemented:
        return not result

    return NotImplemented

Реализация Aaron Halls, __ne__ от not self == other метода __ne__ неверна, поскольку он никогда не может NotImplemented (not NotImplemented is False), и, следовательно, метод __ne__, имеющий приоритет, никогда не может __ne__ метод __ne__, у которого нет приоритета. not self == other раньше был реализацией метода __ne__ по умолчанию в Python 3, но это была ошибка, и она была исправлена в Python 3.4 в январе 2015 года, как заметил ShadowRanger (см. проблему # 21408).

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

Справочник по языку 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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 type(left) != type(right) and 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__ по __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 реализацией 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 реализацией 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, это неверно.

Ответ 5

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