Следует ли реализовать __ne__ как отрицание __eq__?

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

class A:

    def __init__(self, state):
        self.state = state

    def __eq__(self, other):
        return self.state == other.state

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

person Falmarri    schedule 04.12.2010    source источник


Ответы (5)


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

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

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

person Daniel DiPaolo    schedule 04.12.2010
comment
это правильный ответ (здесь, @ aaron-hall). Приведенная вами документация не поощряет вас реализовывать __ne__ с помощью __eq__, только то, что вы это реализуете. - person Guy; 08.09.2016
comment
@guyarad: На самом деле, ответ Аарона все еще немного неверен из-за неправильного делегирования; вместо того, чтобы рассматривать NotImplemented возврат с одной стороны как сигнал для делегирования __ne__ с другой стороны, not self == other (при условии, что __eq__ операнда не знает, как сравнить другой операнд) неявно делегирует __eq__ с другой стороны, а затем инвертирует Это. Для странных типов, например поля ORM SQLAlchemy, это вызывает проблемы. - person ShadowRanger; 18.03.2019
comment
Критика ShadowRanger применима только к очень патологическим случаям (ИМХО) и полностью рассмотрена в моем ответе ниже. - person Aaron Hall; 23.07.2019
comment
Более новая документация (по крайней мере для 3.7, может быть даже раньше) __ne__ автоматически делегирует __eq__, и цитата в этом ответе больше не существует в документации. Суть в том, что это совершенно pythonic - реализовывать только __eq__ и позволять __ne__ делегировать. - person bluesummers; 24.02.2020

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 говорится:

Между операторами сравнения нет подразумеваемых отношений. Истина 18_ не означает, что 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

Реализация 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

Это необходимо для корректности Python API уровня 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.

person Aaron Hall    schedule 05.06.2015
comment
Отличные примеры! Отчасти сюрприз состоит в том, что порядок операндов вообще не имеет значения, в отличие от некоторых магических методов с их отражениями справа. Чтобы повторить ту часть, которую я пропустил (и которая стоила мне много времени): сначала пробуется богатый метод сравнения подкласса, независимо от того, есть ли в коде суперкласс или подкласс на слева от оператора. Вот почему ваш a1 != c2 вернул False --- он запустил не a1.__ne__, а c2.__ne__, что отрицало метод mixin __eq__. Поскольку NotImplemented правдиво, not NotImplemented равно False. - person Kevin J. Chase; 15.03.2016
comment
Ваши недавние обновления успешно демонстрируют преимущество not (self == other) в производительности, но никто не утверждает, что это не быстро (ну, в любом случае, быстрее, чем любой другой вариант на Py2). Проблема в том, что в некоторых случаях это неправильно; Сам Python раньше делал not (self == other), но изменился, потому что он был неправильным при наличии произвольных подклассов. Ответ на самый быстрый неправильный ответ по-прежнему неправильный. - person ShadowRanger; 07.12.2018
comment
@ShadowRanger Я предоставил пример кода, который демонстрирует правильное поведение при действующей реализации __eq__. Бремя доказательства лежит на вас, чтобы предоставить пример кода, демонстрирующий, что not == возвращает недопустимый результат для некоторой (любой возможной) правильной реализации __eq__. - person Aaron Hall; 18.03.2019
comment
@AaronHall: Самый простой пример на самом деле не связан с подклассом. Это неспособность взаимодействовать со странными классами, скажем, классом, для которого и __eq__, и __ne__ безоговорочно возвращают False. В общем, изменение порядка двух экземпляров в сравнении _4 _ / _ 5_ не должно изменять результат, но для вашей реализации, поскольку __ne__ класса Incomparable никогда не вызывается, а __eq__ вашего класса возвращает NotImplemented, __eq__ из Incomparable вызывается, а затем меняет направление, делая результат True только, когда ваш класс один слева, а не справа. - person ShadowRanger; 18.03.2019
comment
Я не могу прямо сейчас придумать связанный с подклассом (возможно, я смешиваю вашу реализацию с другой плохой реализацией, return not self.__eq__(other); я думаю, что ваша может подойти для произвольных подклассов). Но вот пример проблемы асимметрии упомянуто в моем последнем комментарии. - person ShadowRanger; 18.03.2019
comment
Конкретный пример на самом деле не важен. Проблема в том, что в вашей реализации поведение ваших __ne__ делегирует __eq__ (обеих сторон, если необходимо), но оно никогда не возвращается к __ne__ другой стороне, даже когда оба __eq__ сдаются. . Правильный __ne__ делегирует свою собственную __eq__, но если это возвращает NotImplemented, он возвращается к __ne__ другой стороны, вместо того, чтобы инвертировать __eq__ другой стороны (поскольку другая сторона может не указывать явным образом -ed к делегированию __eq__, и вы не должны принимать это решение за него). - person ShadowRanger; 18.03.2019
comment
Я собираюсь обновить свой ответ через день или два, но я считаю ваш пример сравнения патологическим и нереалистичным - возможно, правильнее, что сделать __eq__, - это вызвать исключение. Я действительно думаю, что вам нужно найти пример непатологического подкласса. - person Aaron Hall; 18.03.2019
comment
@AaronHall: при повторном рассмотрении этого сегодня я не думаю, что ваша реализация обычно проблематична для подклассов (было бы чрезвычайно запутанным, чтобы заставить ее сломаться, а подкласс, как предполагается, полностью знает о родителе , должен быть в состоянии избежать этого). Но в своем ответе я просто привел несложный пример. Непатологическим случаем является ORM SQLAlchemy, где ни __eq__, ни __ne__ не возвращают ни True, ни False, а скорее прокси-объект (который оказывается правдивым). Неправильная реализация __ne__ означает, что порядок сравнения имеет значение (вы получаете прокси только в одном порядке). - person ShadowRanger; 18.03.2019
comment
Чтобы было ясно, в 99% (или, может быть, 99,999%) случаев ваше решение в порядке и (очевидно) быстрее. Но поскольку вы не можете контролировать случаи, когда это не нормально, как писатель библиотеки, чей код может использоваться другими (читайте: что угодно, кроме простых одноразовых скриптов и модулей исключительно для личное использование), вы должны использовать правильную реализацию, чтобы придерживаться общего контракта на перегрузку операторов и работать с любым другим кодом, с которым вы можете столкнуться. К счастью, в Py3 все это не имеет значения, поскольку вы можете полностью опустить __ne__. Через год Py2 умрет, и мы игнорируем это. :-) - person ShadowRanger; 18.03.2019
comment
АаронХолл, вам может быть интересен мой обновленный ответ, в котором, я думаю, используется более сильный аргумент, чем аргумент класса Incomparable @ ShadowRanger, поскольку этот класс нарушает связь дополнение между операторами != и == и, следовательно, может считаться патологическим »Пример как ты выразился. И я согласен с тем, что вы правы, когда указываете, что аргумент SQLAlchemy @ ShadowRanger может считаться неуместным, поскольку он находится в не-логическом контексте. - person Maggyero; 02.09.2020

Для записи, канонически правильный и перекрестный переносимый 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), не мешает в некоторых раздражающих / сложных случаях, связанных с сравнениями, когда один из задействованных классов не подразумевает, что результат __ne__ совпадает с результатом not на __eq__ (например, ORM SQLAlchemy, где и __eq__, и __ne__ возвращают специальные прокси-объекты, а не True или False, и попытка not результата __eq__ вернет False, а не правильный прокси-объект).
  • В отличие от not self.__eq__(other), это правильно делегирует __ne__ другого экземпляра, когда self.__eq__ возвращает NotImplemented (not self.__eq__(other) было бы еще более неправильным, потому что NotImplemented является правдой, поэтому, когда __eq__ не знал, как выполнить сравнение, __ne__ вернет False, подразумевая, что два объекта были равны, хотя на самом деле единственный запрошенный объект понятия не имел, что означало бы, что по умолчанию не равно)

Если ваш __eq__ не использует NotImplemented возвраты, это работает (с бессмысленными накладными расходами), если он иногда использует NotImplemented, это обрабатывает его правильно. А проверка версии Python означает, что если класс import-ed в Python 3, __ne__ остается неопределенным, что позволяет использовать собственный эффективный запасной вариант Python _ 29_ реализация (версия C из вышеперечисленного).


Зачем это нужно

Правила перегрузки Python

Объяснение того, почему вы делаете это вместо других решений, несколько загадочно. В Python есть пара общих правил относительно операторов перегрузки и, в частности, операторов сравнения:

  1. (Применимо ко всем операторам) При запуске LHS OP RHS попробуйте LHS.__op__(RHS), а если это вернет NotImplemented, попробуйте RHS.__rop__(LHS). Исключение: если RHS является подклассом класса LHS, сначала проверьте RHS.__rop__(LHS) . В случае операторов сравнения __eq__ и __ne__ являются их собственными "rop" s (так что порядок проверки для __ne__ - LHS.__ne__(RHS), затем RHS.__ne__(LHS), обратный, если RHS является подклассом класса LHS)
  2. Помимо идеи «замененного» оператора, между операторами не существует подразумеваемой связи. Даже для экземпляра того же класса LHS.__eq__(RHS), возвращающий True, не подразумевает, что LHS.__ne__(RHS) возвращает False (на самом деле, операторы даже не обязаны возвращать логические значения; ORM, такие как SQLAlchemy, намеренно этого не делают, что позволяет использовать более выразительный синтаксис запроса). В Python 3 реализация по умолчанию __ne__ ведет себя так, но не по контракту; вы можете переопределить __ne__ способами, которые не являются строгими противоположностями __eq__.

Как это применимо к компараторам с перегрузкой

Итак, когда вы перегружаете оператора, у вас есть две задачи:

  1. Если вы знаете, как реализовать операцию самостоятельно, сделайте это, используя только свои собственные знания о том, как выполнять сравнение (никогда не делегируйте, неявно или явно, другой стороне операции; это рискует некорректность и / или бесконечная рекурсия, в зависимости от того, как вы это делаете)
  2. Если вы не знаете, как реализовать операцию самостоятельно, всегда возвращайте NotImplemented, чтобы Python мог делегировать реализацию другому операнду.

Проблема с not self.__eq__(other)

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

никогда не передает полномочия другой стороне (и неверно, если __eq__ правильно возвращает NotImplemented). Когда self.__eq__(other) возвращает NotImplemented (что является «правдой»), вы молча возвращаетесь False, поэтому A() != something_A_knows_nothing_about возвращает False, когда он должен был проверить, знает ли something_A_knows_nothing_about, как сравнивать с экземплярами A, а если нет, он должен был вернуть True (поскольку, если ни одна из сторон не знает, как сравнивать друг друга, они считаются не равными друг другу). Если A.__eq__ реализован неправильно (возвращает False вместо NotImplemented, когда он не распознает другую сторону), то это "правильно" с точки зрения A, возвращая True (поскольку A не считает это равным, поэтому он не равен), но это могло быть неправильно с точки зрения something_A_knows_nothing_about, так как он никогда даже не спрашивал something_A_knows_nothing_about; A() != something_A_knows_nothing_about заканчивается True, но something_A_knows_nothing_about != A() может False или любое другое возвращаемое значение.

Проблема с not self == other

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

более тонкий. Это будет правильно для 99% классов, включая все классы, для которых __ne__ является логическим обратным __eq__. Но not self == other нарушает оба упомянутых выше правила, что означает, что для классов, где __ne__ не является логическим обратным __eq__, результаты снова несимметричны, потому что один из операндов никогда не запрашивается, если он вообще может реализовать __ne__, даже если другой операнд не может. Простейший пример - это странный класс, который возвращает False для всех сравнений, поэтому оба A() == Incomparable() и A() != Incomparable() возвращают False. При правильной реализации A.__ne__ (та, которая возвращает NotImplemented, когда не знает, как проводить сравнение), связь симметрична; A() != Incomparable() и Incomparable() != A() согласовывают результат (поскольку в первом случае A.__ne__ возвращает NotImplemented, затем Incomparable.__ne__ возвращает False, а во втором Incomparable.__ne__ возвращает False напрямую). Но когда A.__ne__ реализован как return not self == other, A() != Incomparable() возвращает True (поскольку A.__eq__ возвращается, а не NotImplemented, тогда Incomparable.__eq__ возвращает False, а A.__ne__ инвертирует это в True), а Incomparable() != A() возвращает False.

Вы можете увидеть пример этого в действии здесь .

Очевидно, что класс, который всегда возвращает False как для __eq__, так и для __ne__, немного странный. Но, как упоминалось ранее, __eq__ и __ne__ даже не нужно возвращать _115 _ / _ 116_; в SQLAlchemy ORM есть классы с компараторами, которые возвращают специальный прокси-объект для построения запроса, а не _117 _ / _ 118_ вообще (они «правдивы», если оцениваются в логическом контексте, но они никогда не должны оцениваться в таком контексте ).

Если не выполнить правильную перегрузку __ne__, вы сломаете классы такого типа, как показано в коде:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

будет работать (при условии, что SQLAlchemy вообще знает, как вставить MyClassWithBadNE в строку SQL; это можно сделать с помощью адаптеров типов без необходимости MyClassWithBadNE взаимодействия вообще), передав ожидаемый прокси-объект в filter, в то время как:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

закончится передачей filter простого False, потому что self == other возвращает прокси-объект, а not self == other просто преобразует правдивый прокси-объект в False. Надеюсь, filter выдает исключение при обработке недопустимых аргументов, таких как False. Хотя я уверен, что многие будут утверждать, что MyTable.fieldname должен постоянно находиться в левой части сравнения, факт остается фактом: нет никаких программных причин для принудительного применения этого в общем случае, и правильный общий __ne__ будет работать в любом случае, а return not self == other работает только в одной компоновке.

person ShadowRanger    schedule 03.03.2016
comment
Единственно правильный, полный и честный (извините, @AaronHall) ответ. Это должен быть принятый ответ. - person Maggyero; 13.02.2020
comment
Возможно, вас заинтересует мой обновленный ответ, в котором, я думаю, используется более сильный аргумент, чем ваш класс Incomparable, поскольку этот класс нарушает связь дополнение между операторами != и == и поэтому может считаться недопустимым или патологическим примером, например @AaronHall поставил это. И я признаю, что @AaronHall был прав, когда указал, что ваш аргумент SQLAlchemy может считаться неуместным, поскольку он находится в не-логическом контексте. (Ваши аргументы все еще очень интересны и хорошо продуманы.) - person Maggyero; 02.09.2020
comment
+1. Для существующего проекта, который давно забыл реализовать __ne__ для Python 2, я просто ищу __ne__ прокладку, которая лучше всего имитирует поведение Python 3-без-__ne__, чтобы предотвратить регрессию для существующих пользователей Python 3, даже при патологических обстоятельства. Я тестировал решение @AaronHall на нескольких других классах, некоторые, по общему признанию, запутанные, но иногда оно просто не возвращает то же самое, что и Python 3-without-__ne__. Напротив, это решение @ ShadowRanger / @ Maggyero всегда ведет себя точно так же, как Python 3-без-__ne__, независимо от того, какие сумасшедшие вещи я кидаю в него. - person Peter Nowee; 25.11.2020

Правильная __ne__ реализация

Реализация специального метода __ne__ в @ ShadowRanger является правильной:

def __ne__(self, other):
    result = self.__eq__(other)
    if result is not NotImplemented:
        return not result
    return NotImplemented

Это также реализация по умолчанию специального метода __ne__ начиная с Python 3.4, поскольку указано в документации Python:

По умолчанию __ne__() делегирует __eq__() и инвертирует результат, если он не NotImplemented.

Также обратите внимание, что возврат значения NotImplemented для неподдерживаемых операндов не зависит от специального метода __ne__. Фактически, все специальные методы сравнения 1 и специальные числовые методы 2 должны возвращать значение NotImplemented для неподдерживаемых операндов, как указано в теге Документация Python:

Не реализованы

Этот тип имеет единственное значение. Это единственный объект с этим значением. Доступ к этому объекту осуществляется через встроенное имя NotImplemented. Числовые методы и методы расширенного сравнения должны возвращать это значение, если они не реализуют операцию для предоставленных операндов. (Интерпретатор затем попробует отраженную операцию или какой-либо другой запасной вариант, в зависимости от оператора.) Его истинное значение истинно.

Пример специальных числовых методов приведен в Python документация:

class MyIntegral(Integral):

    def __add__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(self, other)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(self, other)
        else:
            return NotImplemented

    def __radd__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(other, self)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(other, self)
        elif isinstance(other, Integral):
            return int(other) + int(self)
        elif isinstance(other, Real):
            return float(other) + float(self)
        elif isinstance(other, Complex):
            return complex(other) + complex(self)
        else:
            return NotImplemented

1 Специальные методы сравнения: __lt__, __le__, __eq__, __ne__, __gt__ и __ge__.

2 Специальные числовые методы: __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__, __mod__, __divmod__, __pow__, __lshift__, __rshift__, __and__, __xor__, __or__ и их __r*__ отраженные и __i*__ на месте аналоги .

Неправильная __ne__ реализация №1

@Falmarri применяет специальный метод __ne__ неверно:

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

Проблема с этой реализацией заключается в том, что она не использует специальный метод __ne__ другого операнда, поскольку он никогда не возвращает значение NotImplemented (выражение not self.__eq__(other) оценивается как значение True или False, в том числе когда его подвыражение self.__eq__(other) оценивается как значение NotImplemented поскольку выражение bool(NotImplemented) оценивается как значение True). Логическое вычисление значения NotImplemented нарушает связь дополнение между операторами сравнения != и ==:

class Correct:

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

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


x, y = Correct(), Correct()
assert (x != y) is not (x == y)

x, y = Incorrect(), Incorrect()
assert (x != y) is not (x == y)  # AssertionError

Неправильная __ne__ реализация №2

Реализация специального метода __ne__ в @AaronHall также неверна:

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

Проблема с этой реализацией заключается в том, что она напрямую обращается к специальному методу __eq__ другого операнда, минуя специальный метод __ne__ другого операнда, поскольку он никогда не возвращает значение NotImplemented (выражение not self == other возвращается к специальному методу __eq__ из другой операнд и оценивается как значение True или False). Обход метода неверен, поскольку этот метод может иметь побочные эффекты как обновление состояния объекта:

class Correct:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        return not self == other


x, y = Correct(), Correct()
assert x != y
assert x.counter == y.counter

x, y = Incorrect(), Incorrect()
assert x != y
assert x.counter == y.counter  # AssertionError

Понимание операций сравнения

В математике двоичное отношение R над набором X - это набор упорядоченных пар (x, y) в X 2 . Выражение (x, y) в R читается как x и связано с R до y и обозначается xRy.

Свойства бинарного отношения R над набором X:

  • R является рефлексивным, когда для всех x в X, xRx.
  • R является нерефлексивным ( также называется strict), когда для всех x в X, а не xRx.
  • R является симметричным, когда для всех x и y в X, если xRy, то yRx.
  • R является антисимметричным, когда для всех x и y в X, если xRy и yRx, то x = г.
  • R является транзитивным, когда для всех x, y и z в X, если xRy и yRz , затем xRz.
  • R - это connex (также называемый total), когда для всех x и y в X, xRy или YRx.
  • R - это отношение эквивалентности, когда R является рефлексивным, симметричным и транзитивным.
    Например, =. Однако только симметрично.
  • R - это отношение порядка, когда R рефлексивно, антисимметрично и транзитивно.
    Например, ≤ и ≥.
  • R - это отношение строгого порядка , когда R является иррефлексивным, антисимметричным и транзитивным.
    Например, ‹и›. Однако ≠ только иррефлексивно.

Операции с двумя бинарными отношениями R и S над набором X:

  • преобразование R является бинарным отношением R T = {(y, x) | xRy} поверх X.
  • дополнение R является бинарным отношением ¬ R = {(x, y) | не xRy} над X.
  • объединение R и S - бинарное отношение RS = {(x, y ) | xRy или xSy} поверх X.

Отношения между отношениями сравнения, которые всегда действительны:

  • 2 дополнительных отношения: = и ≠ дополняют друг друга;
  • 6 обратных отношений: = является обратным самому себе, ≠ является обратным самому себе, ‹и› являются обратными друг другу, а ≤ и ≥ являются обратными друг другу;
  • 2 отношения объединения: ≤ - это объединение ‹и =, а ≥ - объединение› и =.

Связи между отношениями сравнения, которые действительны только для заказов connex:

  • 4 дополнительных отношения: ‹и ≥ дополняют друг друга, а› и ≤ дополняют друг друга.

Таким образом, чтобы правильно реализовать в Python операторы сравнения ==, !=, <, >, <= и >=, соответствующие отношениям сравнения =, ≠, ‹,›, ≤ и ≥, все вышеуказанные математические свойства и отношения должны выполняться.

Операция сравнения x operator y вызывает специальный метод сравнения __operator__ класса одного из его операндов:

class X:

    def __operator__(self, other):
        # implementation

Поскольку R является рефлексивным, подразумевает xRx, рефлексивную операцию сравнения x operator y (x == y, x <= y и x >= y) или рефлексивный вызов специального метода сравнения x.__operator__(y) (x.__eq__(y), x.__le__(y) и x.__ge__(y)) должны оцениваться как значение True, если x и y идентичны, то есть если выражение x is y оценивается как True. Поскольку R является нерефлексивным, подразумевает не xRx, операция нерефлексивного сравнения x operator y (x != y, x < y и x > y) или нерефлексивный вызов специального метода сравнения x.__operator__(y) (x.__ne__(y) , x.__lt__(y) и x.__gt__(y)) должны оцениваться как значение False, если x и y идентичны, то есть если выражение x is y оценивается как True. Рефлексивное свойство рассматривается Python для оператора сравнения == и связанного с ним специального метода сравнения __eq__, но неожиданно не учитывается для операторов сравнения <= и >= и связанных с ними специальных методов сравнения __le__ и __ge__, а свойство нерефлексивности рассматривается Python для оператор сравнения != и связанный с ним специальный метод сравнения __ne__, но неожиданно не учитывается для операторов сравнения < и > и связанных с ними специальных методов сравнения __lt__ и __gt__. Вместо этого игнорируемые операторы сравнения вызывают исключение TypeError (и связанные специальные методы сравнения вместо этого возвращают значение NotImplemented), как описано в документация Python:

Поведение по умолчанию для сравнения на равенство (== и !=) основано на идентичности объектов. Следовательно, сравнение на равенство экземпляров с одним и тем же идентификатором приводит к равенству, а сравнение на равенство экземпляров с разными идентификаторами приводит к неравенству. Мотивацией для этого поведения по умолчанию является желание, чтобы все объекты были рефлексивными (т.е. x is y подразумевает x == y).

Сравнение порядка по умолчанию (<, >, <= и >=) не предусмотрено; попытка поднимает TypeError. Мотивацией для такого поведения по умолчанию является отсутствие инварианта, аналогичного равенству. [Это неверно, поскольку <= и >= рефлексивны, как ==, а < и > нерефлексивны, как !=.]

Класс object предоставляет реализации по умолчанию специальных методов сравнения, которые наследуются всеми его подклассами, как описано в Документация по Python:

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

Поскольку R = (R T) T, сравнение xRy эквивалентно обратное сравнение yR T x (неофициальное название отражено в документации Python). Таким образом, есть два способа вычислить результат операции сравнения x operator y: вызвать либо x.__operator__(y), либо y.__operatorT__(x). Python использует следующую вычислительную стратегию:

  1. Он вызывает x.__operator__(y), если класс правого операнда не является потомком класса левого операнда. В этом случае он вызывает y.__operatorT__(x) (позволяя классам переопределять специальный обратный метод сравнения своих предков).
  2. Если операнды x и y не поддерживаются (на это указывает возвращаемое значение NotImplemented), он вызывает обратный специальный метод сравнения как 1-й резерв.
  3. Если операнды x и y не поддерживаются (на это указывает возвращаемое значение NotImplemented), он вызывает исключение TypeError за исключением операторов сравнения == и !=, для которых он сравнивает, соответственно, идентичность и неидентичность операндов x и y как < em> 2-й запасной вариант (с использованием свойства рефлексивности == и свойства нерефлексивности !=).
  4. Возвращает результат.

В CPython это реализовано в коде C, который можно перевести в код Python (с именами eq для ==, ne для !=, lt для <, gt для >, le для <= и ge для >=):

def 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 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 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def 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__}' "
            f"and '{type(right).__name__}'"
        )
    return result

Поскольку R = ¬ (¬ R), сравнение xRy эквивалентно дополнительному сравнению ¬ ( х ¬ Ry). ≠ является дополнением к =, поэтому специальный метод __ne__ реализуется в терминах специального метода __eq__ для поддерживаемых операндов по умолчанию, в то время как другие специальные методы сравнения реализуются независимо по умолчанию (факт, что ≤ является объединением ‹и = , а ≥ является объединением ›и = является на удивление не рассматривается, что означает, что в настоящее время специальные методы __le__ и __ge__ должны быть реализованы пользователем), как описано в Документация Python:

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

В CPython это реализовано в коде C, который можно перевести в код Python:

def __eq__(self, other):
    return self is other or 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

Итак, по умолчанию:

  • операция сравнения x operator y вызывает исключение TypeError за исключением операторов сравнения == и !=, для которых она возвращает соответственно значения True и False, если операнды x и y соответственно идентичны и неидентичны, и значения False и True в противном случае;
  • вызов специального метода сравнения x.__operator__(y) возвращает значение NotImplemented, за исключением специальных методов сравнения __eq__ и __ne__, для которых он возвращает соответственно значения True и False, если операнды x и y соответственно идентичны и неидентичны, и значение NotImplemented в противном случае.
person Maggyero    schedule 02.06.2018
comment
К вашему последнему примеру: поскольку эта реализация не может воспроизвести поведение стандартной реализации метода __ne__, когда метод __eq__ возвращает NotImplemented, это неверно. - A определяет безусловное равенство. Таким образом, A() == B(). Таким образом, A() != B() должно быть False, а это равно. Приведенные примеры являются патологическими (т.е. __ne__ не должен возвращать строку, а __eq__ не должен зависеть от __ne__ - скорее __ne__ должен зависеть от __eq__, что является ожиданием по умолчанию в Python 3). У меня по-прежнему -1 к этому ответу, пока вы не передумаете. - person Aaron Hall; 16.02.2020
comment
В последнем примере есть два класса: B, который возвращает правдивую строку для всех проверок для __ne__, и A, который возвращает True для всех проверок для __eq__. Это патологическое противоречие. При таком противоречии лучше всего сделать исключение. Без знания B, A не обязан уважать реализацию __ne__ в B в целях симметрии. На этом этапе примера мне не важно, как A реализует __ne__. Пожалуйста, найдите практический, непатологический случай, чтобы изложить свою точку зрения. Я обновил свой ответ, чтобы обратиться к вам. - person Aaron Hall; 16.02.2020
comment
Вариант использования SQLAlchemy - это язык, специфичный для домена. Если кто-то разрабатывает такой DSL, можно выбросить все советы здесь в окно. Продолжая мучить эту неудачную аналогию, ваш пример ожидает, что самолет будет лететь назад половину времени, а мой ожидает, что они будут лететь только вперед, и я думаю, что это разумное дизайнерское решение. Я думаю, что обеспокоенность, которую вы поднимаете, необоснованна и обратна. - person Aaron Hall; 17.02.2020

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

person Karl Knechtel    schedule 04.12.2010
comment
Специальный метод __cmp__() больше не поддерживается в Python 3.x, поэтому вам следует привыкнуть к использованию расширенных операторов сравнения. - person Don O'Donnell; 04.12.2010
comment
Или, как альтернатива, если вы используете Python 2.7 или 3.x, декоратор functools.total_ordering также весьма удобен. - person Adam Parkin; 11.07.2012
comment
Спасибо за предупреждение. Однако за последние полтора года я понял многое в этом направлении. ;) - person Karl Knechtel; 12.07.2012