Должен ли я реализовать __ne__ в терминах __eq__ в Python?

101

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

class A:

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

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

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

Фалмарри
источник

Ответы:

60

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

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

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

Даниэль ДиПаоло
источник
12
это правильный ответ (здесь, @ aaron-hall). Приведенная вами документация не поощряет вас к реализации __ne__using __eq__, а только ее реализации.
гайарад 08
2
@guyarad: На самом деле, ответ Аарона все еще немного неверен из-за неправильного делегирования; вместо того, чтобы рассматривать NotImplementedвозврат с одной стороны как сигнал для делегирования __ne__другой стороне, not self == other(при условии, что операнд __eq__не знает, как сравнить другой операнд) неявно делегирует __eq__другой стороне, а затем инвертирует его. Для странных типов, например полей ORM SQLAlchemy, это вызывает проблемы .
ShadowRanger
1
Критика ShadowRanger применима только к очень патологическим случаям (ИМХО) и полностью рассмотрена в моем ответе ниже.
Аарон Холл
2
Более новая документация (по крайней мере для 3.7, может быть даже раньше) __ne__автоматически делегируется, __eq__и цитата в этом ответе больше не существует в документах. Суть в том, что это совершенно pythonic только для реализации __eq__и __ne__делегирования.
блюз-лето
136

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

Краткий ответ: не внедряйте, но если нужно, используйте ==, а не__eq__

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

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

То есть имейте в виду комментарий Раймона Хеттингера :

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

Если вам нужно, чтобы ваш код работал на Python 2, следуйте рекомендациям для Python 2, и он будет отлично работать на Python 3.

В Python 2 сам Python не реализует автоматически какую-либо операцию в терминах другой, поэтому вам следует определять __ne__in в терминах ==вместо __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.

а в разделе «Что нового» мы видим, что поведение изменилось:

  • !=now возвращает обратное ==, если не ==возвращает NotImplemented.

Для реализации __ne__мы предпочитаем использовать ==оператор вместо использования __eq__метода напрямую, чтобы, если self.__eq__(other)подкласс возвращает NotImplementedпроверенный тип, Python соответствующим образом проверит other.__eq__(self) Из документации :

NotImplementedобъект

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

Когда дается богатый оператор сравнения, если они не тот же самый тип, Python проверяет , является ли otherэто подтип, и если у него есть , что оператор , определенный, он использует otherпервый метод «s (обратный для <, <=, >=и >). Если NotImplementedвозвращается, то используется противоположный метод. (Он не проверяет один и тот же метод дважды.) Использование ==оператора позволяет реализовать эту логику.


Ожидания

Семантически вы должны реализовать __ne__проверку на равенство, потому что пользователи вашего класса будут ожидать, что следующие функции будут эквивалентны для всех экземпляров A:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

То есть обе указанные выше функции всегда должны возвращать один и тот же результат. Но это зависит от программиста.

Демонстрация неожиданного поведения при определении __ne__на основе __eq__:

Сначала настройка:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

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

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

Создайте неэквивалентные экземпляры:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

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

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

Эти экземпляры __ne__реализованы с помощью ==:

assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1

Эти экземпляры, тестируемые под Python 3, также работают правильно:

assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1

И помните, что они __ne__реализованы с __eq__- хотя это ожидаемое поведение, реализация неверна:

assert not wrong1 == wrong2         # These are contradicted by the
assert not wrong2 == wrong1         # below unexpected behavior!

Неожиданное поведение:

Обратите внимание, что это сравнение противоречит приведенным выше сравнениям ( not wrong1 == wrong2).

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

а также,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Не пропускайте __ne__Python 2

Для доказательства того, что вам не следует отказываться от реализации __ne__в Python 2, см. Эти эквивалентные объекты:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

Результат должен быть выше False!

Исходный код Python 3

Реализация CPython по умолчанию для __ne__находится typeobject.cвobject_richcompare :

case Py_NE:
    /* By default, __ne__() delegates to __eq__() and inverts the result,
       unless the latter returns NotImplemented. */
    if (Py_TYPE(self)->tp_richcompare == NULL) {
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }
    res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
    if (res != NULL && res != Py_NotImplemented) {
        int ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        if (ok < 0)
            res = NULL;
        else {
            if (ok)
                res = Py_False;
            else
                res = Py_True;
            Py_INCREF(res);
        }
    }
    break;

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

Детали__ne__ реализации Python 3 по умолчанию на уровне C используются, __eq__потому что более высокий уровень ==( PyObject_RichCompare ) будет менее эффективным - и, следовательно, он также должен обрабатывать NotImplemented.

Если __eq__реализовано правильно, то отрицание ==также верно - и это позволяет нам избежать деталей реализации низкого уровня в нашем __ne__.

Использование ==позволяет нам хранить нашу низкоуровневую логику в одном месте и избегать обращения NotImplementedк ним __ne__.

Можно ошибочно предположить, что это ==может вернуться NotImplemented.

Фактически он использует ту же логику, что и реализация по умолчанию __eq__, которая проверяет идентичность (см. Do_richcompare и наши доказательства ниже)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

И сравнения:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

Производительность

Не верьте мне на слово, давайте посмотрим, что более производительно:

class CLevel:
    "Use default logic programmed in C"

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

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

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

Я думаю, что эти показатели производительности говорят сами за себя:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

Это имеет смысл, если учесть, что low_level_pythonв Python выполняется логика, которая в противном случае обрабатывалась бы на уровне C.

Ответ некоторым критикам

Другой отвечающий пишет:

Реализация Аарона Холл not self == otherиз __ne__метода некорректна , так как он никогда не сможет вернуться NotImplemented( not NotImplementedв False) и , следовательно, __ne__метод , который имеет приоритет никогда не может упасть обратно на __ne__методе , который не имеет приоритета.

То, что вы __ne__никогда не вернетесь NotImplemented, не делает его неправильным. Вместо этого мы обрабатываем приоритизацию с NotImplementedпомощью проверки на равенство с ==. Предполагая ==, что все выполнено правильно, мы закончили.

not self == otherРаньше __ne__это была реализация метода по умолчанию в Python 3, но это была ошибка, и она была исправлена ​​в Python 3.4 в январе 2015 года, как заметил ShadowRanger (см. проблему № 21408).

Что ж, давайте это объясним.

Как отмечалось ранее, Python 3 по умолчанию обрабатывает __ne__, сначала проверяя, self.__eq__(other)возвращает ли он NotImplemented(синглтон), что следует проверить с помощью isи вернуть, если да, иначе он должен вернуть обратное. Вот эта логика, написанная как миксин классов:

class CStyle__ne__:
    """Mixin that provides __ne__ functionality equivalent to 
    the builtin functionality
    """
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

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

Аарон Холл
источник
3
Отличные примеры! Отчасти сюрприз состоит в том, что порядок операндов вообще не имеет значения , в отличие от некоторых магических методов с их «правосторонним» отражением. Чтобы повторить ту часть, которую я пропустил (и которая стоила мне много времени): сначала проверяется богатый метод сравнения подкласса , независимо от того, есть ли в коде суперкласс или подкласс слева от оператора. Вот почему вы a1 != c2вернули False--- он не запустился a1.__ne__, но c2.__ne__, что отрицает метод миксина __eq__ . Поскольку NotImplementedэто правда, то not NotImplementedесть False.
Кевин Дж. Чейз
2
Ваши недавние обновления действительно демонстрируют преимущество в производительности not (self == other), но никто не утверждает, что это не быстро (ну, в любом случае, быстрее, чем любой другой вариант на Py2). Проблема в том, что в некоторых случаях это неправильно ; Сам Python имел обыкновение делать not (self == other), но изменился, потому что он был неправильным при наличии произвольных подклассов . Ответ на быстрый неверный ответ все равно неверен .
ShadowRanger
1
Конкретный пример на самом деле не важен. Проблема в том, что в вашей реализации поведение ваших __ne__делегатов __eq__(обеих сторон, если необходимо), но никогда не возвращается обратно к __ne__другой стороне, даже когда обе __eq__«сдаются». Правильные __ne__делегаты для себя __eq__ , но если это вернется NotImplemented, он возвращается к другой стороне __ne__, а не инвертирует другую сторону __eq__(поскольку другая сторона может явно не участвовать в делегировании __eq__, и вы не должны принимать за это решение).
ShadowRanger
1
@AaronHall: при повторном рассмотрении этого сегодня, я не думаю, что ваша реализация обычно проблематична для подклассов (было бы чрезвычайно запутанным, чтобы заставить ее сломаться, и подкласс, который, как предполагается, имеет полное знание о родителе, должен иметь возможность избежать этого ). Но я просто привел в своем ответе несложный пример. Непатологическим случаем является ORM SQLAlchemy, где ни и __eq__не __ne__возвращает ни Trueили False, а скорее прокси-объект (который оказывается «правдивым»). Неправильная реализация __ne__означает, что порядок сравнения имеет значение для сравнения (вы получаете прокси только в одном порядке).
ShadowRanger
1
Чтобы было ясно, в 99% (или, может быть, 99,999%) случаев ваше решение в порядке и (очевидно) быстрее. Но поскольку у вас нет контроля над случаями, когда это не нормально, как писатель библиотеки, чей код может использоваться другими (читайте: все, кроме простых одноразовых скриптов и модулей исключительно для личного использования), используйте правильную реализацию, чтобы придерживаться общего контракта на перегрузку операторов и работать с любым другим кодом, с которым вы можете столкнуться. К счастью, на Py3 все это не имеет значения, так как вы можете __ne__полностью опустить . Через год Py2 умрет, и мы игнорируем это. :-)
ShadowRanger
10

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

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


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

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

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

  1. (Относится ко всем операторам) При запуске LHS OP RHSпопробуйте LHS.__op__(RHS), а если вернется NotImplemented, попробуйте RHS.__rop__(LHS). Исключение: если RHSэто подкласс LHSкласса, RHS.__rop__(LHS) сначала проверьте . В случае операторов сравнения, __eq__и __ne__их собственный «ПРП» s (так что тест заказ на __ne__это LHS.__ne__(RHS), то RHS.__ne__(LHS), сторнируется , если RHSэто подкласс LHSкласса «s)
  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__даже не нужно возвращать True/ False; ORM SQLAlchemy имеет классы с компараторами, которые возвращают специальный прокси-объект для построения запроса, а не True/ Falseвообще (они «правдивы», если оцениваются в логическом контексте, но они никогда не должны оцениваться в таком контексте).

Будучи не в состоянии перегрузки __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работает только в одной аранжировке.

ShadowRanger
источник
1
Единственно правильный, полный и честный (извините, @AaronHall) ответ. Это должен быть принятый ответ.
Maggyero
Вы можете быть заинтересованы мой обновленный ответ , который использует я думаю , что более сильный аргумент , чем ваш Incomparableкласс , так как данный класс перерывов в дополнение отношения между !=и ==операторов , и поэтому может рассматриваться как недействительное или «патологической» пример , как @AaronHall выразился. И я признаю, что @AaronHall был прав, когда указал, что ваш аргумент SQLAlchemy может считаться неуместным, поскольку он находится в не-логическом контексте. (Ваши аргументы все еще очень интересны и хорошо продуманы.)
Maggyero
4

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

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

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

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

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 - это набор упорядоченных пар ( xy ) в  X 2 . Выражение ( xy ) в  R читается как « x является R- связано с y » и обозначается xRy .

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

  • R является рефлексивным , когда для всех х в X , XRX .
  • R является иррефлексивным (также называемым строгим ), когда для всех x из X , а не xRx .
  • R является симметричным, когда для всех x и y в X , если xRy, то yRx .
  • R является антисимметричным , когда для всех х и у в X , если хКа и уая тогда х  =  у .
  • R является транзитивным , если для всех х , у и г в X , если хКу и yRz затем xRz .
  • R - это коннекс (также называемый общим ), когда для всех x и y в X , xRy или yRx .
  • R является отношением эквивалентности, когда R рефлексивно, симметрично и транзитивно.
    Например, =. Однако только симметрично.
  • R является отношением порядка, когда R рефлексивно, антисимметрично и транзитивно.
    Например, ≤ и ≥.
  • R является отношением строгого порядка, когда R иррефлексивно, антисимметрично и транзитивно.
    Например, <и>. Однако ≠ только иррефлексивно.

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

  • Обратное из R представляет собой бинарное отношение R T  = {( ух ) | хКу } над X .
  • Дополнение из R представляет собой бинарное отношение ¬ R  = {( ху ) | не хКа } над X .
  • Объединение из R и S представляет собой бинарное отношение R  ∪  S  = {( ху ) | хКа или вании } над 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как 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 эквивалентно сравнению с дополнением ¬ ( x ¬ 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за исключением операторов сравнения ==и !=для которых она возвращает идентичность и неидентичность операндов xи y;
  • вызов специального метода сравнения x.__operator__(y)возвращает значение, NotImplementedза исключением специальных методов сравнения __eq__и __ne__для которых он возвращает соответственно, Trueи Falseесли операнды xи yсоответственно идентичны и неидентичны, и значение в NotImplementedпротивном случае.
Maggyero
источник
К вашему последнему примеру: «Поскольку эта реализация не может воспроизвести поведение реализации __ne__метода по умолчанию, когда __eq__метод возвращает NotImplemented, это неверно». - Aопределяет безусловное равенство. Таким образом, A() == B(). Таким образом , A() != B() должно быть ложным , и есть . Приведенные примеры являются патологическими (т.е. __ne__не должны возвращать строку и __eq__не должны зависеть от __ne__- скорее, __ne__должны зависеть от __eq__, что является ожиданием по умолчанию в Python 3). Я по-прежнему -1 к этому ответу, пока вы не передумаете.
Аарон Холл
@AaronHall Из справочника по языку Python : «Богатый метод сравнения может возвращать синглтон, NotImplementedесли он не реализует операцию для данной пары аргументов. По соглашению Falseи Trueвозвращаются для успешного сравнения. Однако эти методы могут возвращать любое значение. , поэтому, если оператор сравнения используется в логическом контексте (например, в условии оператора if), Python вызовет bool()значение, чтобы определить, является ли результат истинным или ложным ».
Maggyero
В последнем примере есть два класса, Bкоторые возвращают правдивую строку при всех проверках для __ne__и Aвозвращают Trueпри всех проверках __eq__. Это патологическое противоречие. При таком противоречии лучше всего было бы сделать исключение. Не зная B, Aне обязан соблюдать Bреализацию __ne__в целях симметрии. На этом этапе примера мне неважно , как работает Aорудие __ne__. Пожалуйста, найдите практический, непатологический случай, чтобы выразить свою точку зрения. Я обновил свой ответ, чтобы обратиться к вам.
Аарон Холл
Вариант использования SQLAlchemy - это язык, специфичный для домена. Если кто-то разрабатывает такой DSL, можно выбросить все советы здесь в окно. Продолжая мучить эту неудачную аналогию, ваш пример предполагает, что самолет летит назад половину времени, а мой ожидает, что они летят только вперед, и я думаю, что это разумное дизайнерское решение. Я думаю, что обеспокоенность, которую вы поднимаете, необоснованна и обратна.
Аарон Холл
-1

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

Карл Кнехтель
источник
12
Этот __cmp__()специальный метод больше не поддерживается в Python 3.x, поэтому вам следует привыкнуть к использованию разнообразных операторов сравнения.
Дон О'Доннелл,
8
Или же, если вы используете Python 2.7 или 3.x, декоратор functools.total_ordering также очень удобен.
Adam Parkin
Спасибо за предупреждение. Однако за последние полтора года я понял многое в этом направлении. ;)
Karl Knechtel