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-only не пишите один, если вам не нужно затмить родителя реализация, например, для встроенного объекта.

то есть, имейте в виду компания Raymond Hettinger это:

на __ne__ метод следует автоматически из __eq__ только если __ne__ еще не определен в суперклассе. Так что, если вы наследуя от builtin, лучше всего переопределить оба.

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

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

реализация 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 in __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-это даже больше простой и работоспособный (хотя программист отвечает за определение того, что это правильно).

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


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


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

хотя интересно, ответ Аарона холла не является правильным способом реализации __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 если он не реализует операцию для данной пары аргументов.

там не являются ли версии этих методов swapped-argument (для использования когда левый аргумент не поддерживает операцию, а правый аргумент делает); скорее,__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 is False (as bool(NotImplemented) и True) вместо желаемого NotImplemented.

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

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

  • the __eq__ возвращает NotImplemented;
  • the __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, the 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, это неправильно.