Python, чи варто реалізовувати оператор __ne __ () на основі __eq__?


98

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

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

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

Або є щось, чого мені не вистачає з тим, як Python використовує ці оператори, що робить це не найкращою ідеєю?

Відповіді:


57

Так, це чудово. Фактично, документація вимагає визначити, __ne__коли ви визначаєте __eq__:

Між операторами порівняння не маються на увазі зв’язки. Істина x==yне означає, що x!=y це помилково. Відповідно, визначаючи __eq__(), слід також визначитись __ne__()так, що оператори будуть вести себе так, як очікувалося.

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


12
це правильна відповідь (тут внизу, @ aaron-hall). Документація, яку ви цитували, не заохочує вас __ne__використовувати __eq__, лише те, що ви її реалізуєте.
gyarad

2
@guyarad: Насправді відповідь Аарона все ще дещо неправильна завдяки неправильному делегуванню; замість того, щоб розглядати NotImplementedповернення з однієї сторони як підказку для делегування __ne__іншій стороні, not self == other(припускаючи, що операнд __eq__не знає, як порівнювати інший операнд), неявно делегує __eq__з іншої сторони, а потім інвертує його. Для дивних типів, наприклад, поля SQLAlchemy ORM, це спричиняє проблеми .
ShadowRanger

1
Критика ShadowRanger стосується лише дуже патологічних випадків (IMHO) і повністю розглядається в моїй відповіді нижче.
Аарон Холл

1
Новіші документації (принаймні для 3.7, можливо, навіть раніше) __ne__делегуються автоматично, __eq__і цитата у цій відповіді більше не існує в документах. Підсумок - це абсолютно пітонічно лише реалізовувати __eq__та нехай __ne__делегувати.
блюзмерс

132

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

Коротка відповідь: Не впроваджуйте це, але якщо потрібно, використовуйте ==, ні__eq__

У Python 3 !=це заперечення ==за замовчуванням, тому вам навіть не потрібно писати a __ne__, і документація більше не має думки щодо його написання.

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

Тобто майте на увазі коментар Реймонда Хеттінгера :

__ne__Метод автоматично випливає з __eq__тільки , якщо __ne__ще не визначена в суперкласу. Отже, якщо ви успадковуєте вбудовану версію, найкраще замінити обидва.

Якщо вам потрібен ваш код для роботи в Python 2, дотримуйтесь рекомендацій для Python 2, і він буде добре працювати в Python 3.

У Python 2, сам Python автоматично не реалізує жодну операцію в іншому, тому слід визначити __ne__терміни ==замість __eq__. EG

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

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

Дивіться доказ цього

  • реалізатор __ne__()оператора на основі __eq__та
  • взагалі не реалізується __ne__в Python 2

подано неправильну поведінку в демонстрації нижче.

Довга відповідь

Документація для Python 2 говорить:

Між операторами порівняння не маються на увазі зв’язки. Істина x==yне означає, що x!=yце помилково. Відповідно, визначаючи __eq__(), слід також визначитись __ne__()так, що оператори будуть вести себе так, як очікувалося.

Отже, це означає, що якщо ми визначимось __ne__із оберненою стороною __eq__, ми можемо отримати послідовну поведінку.

Цей розділ документації оновлено для Python 3:

За замовчуванням __ne__()делегує результат __eq__()та інвертує результат, якщо він не є NotImplemented.

і в розділі "Що нового" ми бачимо, що така поведінка змінилася:

  • !=тепер повертає протилежне ==, якщо не ==повертається NotImplemented.

Для реалізації __ne__ми віддаємо перевагу використовувати ==оператор, а не __eq__метод безпосередньо, щоб, якщо self.__eq__(other)підклас повертається NotImplementedдля перевіреного типу, Python належним чином перевіряв other.__eq__(self) з документації :

NotImplementedоб'єкт

Цей тип має єдине значення. Існує один об’єкт із цим значенням. Доступ до цього об'єкта здійснюється через вбудовану назву NotImplemented. Числові методи та розширені методи порівняння можуть повернути це значення, якщо вони не реалізують операцію для наданих операндів. (Потім інтерпретатор спробує відображену операцію або будь-який інший резервний варіант, залежно від оператора.) Її істинність є істинною.

Коли дається багатий оператор порівняння, якщо вони не той же самий тип, Python перевіряє , є чи otherце підтип, і якщо у нього є , що оператор , визначений, він використовує otherперший метод «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

Це необхідно для коректності для API рівня Python на рівні 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__такої ж , як результат notна __eq__(ОРЗ , наприклад , 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__буде визначений, що дозволяє переймати натурну ефективну __ne__реалізацію резервної копії Python (версія C вище) .


Навіщо це потрібно

Правила перевантаження Python

Пояснення, чому ви робите це замість інших рішень, дещо загадкове. У Python є кілька загальних правил щодо операторів перевантаження, зокрема операторів порівняння:

  1. (Застосовується для всіх операторів) Під час запуску LHS OP RHSспробуйте LHS.__op__(RHS), і якщо це повернеться NotImplemented, спробуйте RHS.__rop__(LHS). Виняток: Якщо RHSце підклас класу LHS's, 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(що є "truthy"), ви мовчки повертаєтесь 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__, навіть якщо інший операнд не може. Найпростіший приклад - клас weirdo, який повертається 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просто перетворює об'єкт truthy проксі False. Сподіваємось, filterвикидає виняток за обробку недійсних аргументів, як False. Хоча я впевнений, що багато хто буде стверджувати, що це MyTable.fieldname має бути послідовно лівою стороною порівняння, факт залишається фактом: немає програмних підстав для забезпечення цього в загальному випадку, і правильний загальний __ne__засіб буде працювати в будь-якому випадку, тоді return not self == otherяк працює в одному розташуванні.


1
Єдина правильна, повна та чесна (вибачте @AaronHall) відповідь. Це має бути прийнятою відповіддю.
Maggyero

4

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

Реалізація __ne__методу ShadowRanger є правильною (і це, як правило, реалізація __ne__методу за замовчуванням з Python 3.4):

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

    if result is not NotImplemented:
        return not result

    return NotImplemented

Чому? Тому що він зберігає важливе математичне властивість, то симетрію від !=оператора. Цей оператор є двійковим, тому його результат повинен залежати від динамічного типу обох операндів, а не лише одного. Це реалізовано за допомогою подвійного відправлення для мов програмування, що дозволяє здійснювати багаторазове відправлення (наприклад, Джулія ). У Python, який допускає лише одне відправлення, подвійне відправлення моделюється для числових методів та розширених методів порівняння , повертаючи значення NotImplementedв методах реалізації, які не підтримують тип іншого операнда; Потім інтерпретатор спробує відображений метод іншого операнда.

Реалізація Аарона Хол not self == otherз __ne__методу є неправильною , оскільки це знімає симетрію !=оператора. Дійсно, він ніколи не може повернутися NotImplemented( not NotImplementedє False), і тому __ne__метод з більш високим пріоритетом ніколи не може повернутися до __ne__методу з нижчим пріоритетом. not self == otherраніше була реалізацією __ne__методу Python 3 за замовчуванням, але це була помилка, яку було виправлено в Python 3.4 в січні 2015 року, як помітив ShadowRanger (див. випуск № 21408 ).

Впровадження операторів порівняння

Python Language Reference для Python 3 держави в своїй голові моделі III даних :

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

Це так звані методи «багатого порівняння». Відповідність між символами оператора та назвами методів така: x<yдзвінки x.__lt__(y), x<=yдзвінки x.__le__(y), x==yдзвінки x.__eq__(y), x!=yдзвінки x.__ne__(y), x>yдзвінки x.__gt__(y)та x>=y дзвінки x.__ge__(y).

Розширений метод порівняння може повернути синглтон, NotImplementedякщо він не реалізує операцію для заданої пари аргументів.

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

Переведення цього коду в Python дає (використовуючи operator_eqдля ==, operator_neдля !=, operator_ltдля <, operator_gtдля >, operator_leдля <=і operator_geдля >=):

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

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

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

    if result is NotImplemented:
        result = left is right

    return result


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

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

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

    if result is NotImplemented:
        result = left is not right

    return result


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

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

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

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

    return result


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

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

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

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

    return result


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

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

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

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

    return result


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

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

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

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

    return result

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

Документація додає:

За замовчуванням __ne__()делегує результат __eq__()та інвертує результат, якщо він не є NotImplemented. Інших мається на увазі взаємозв'язків між операторами порівняння, наприклад, правда (x<y or x==y)не передбачає x<=y.

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

def __eq__(self, other):
    return NotImplemented

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

    if result is not NotImplemented:
        return not result

    return NotImplemented

def __lt__(self, other):
    return NotImplemented

def __gt__(self, other):
    return NotImplemented

def __le__(self, other):
    return NotImplemented

def __ge__(self, other):
    return NotImplemented

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

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

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

  • то __eq__метод повертає NotImplemented;
  • __eq__метод повертає інше значення з NotImplemented.

Реалізація за замовчуванням

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

class A:
    pass


class B:

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


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

Це показує, що коли A.__eq__метод повертається NotImplemented, A.__ne__метод повертається до B.__ne__методу.

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

class A:

    def __eq__(self, other):
        return True


class B:

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


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

Це показує, що в цьому випадку A.__ne__метод повертає метод, зворотний до A.__eq__методу. Таким чином, __ne__метод поводиться так, як рекламується в документації.

Переопределення A.__ne__методу за замовчуванням методу при правильній реалізації, наведеній вище, дає ті ж результати.

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

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

class A:

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


class B:

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


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

Запроваджена реалізація __ne__методу повернута "B.__ne__", ні True.

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

class A:

    def __eq__(self, other):
        return True

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


class B:

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


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

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

Оскільки ця реалізація не вдається відтворити поведінку реалізації __ne__методу за замовчуванням, коли __eq__метод повертається NotImplemented, це неправильно.


До вашого останнього прикладу: "Оскільки ця реалізація не може повторити поведінку методу за замовчуванням, __ne__коли __eq__метод повертає NotImplemented, це неправильно." - Aвизначає безумовну рівність. Таким чином, A() == B(). Таким чином, A() != B() повинно бути помилковим , і воно є . Наведені приклади є патологічними (тобто __ne__не повинні повертати рядок і __eq__не повинні залежати __ne__- скоріше __ne__повинні залежати від того __eq__, що є очікуванням за замовчуванням у Python 3). Я все ще -1 на цю відповідь, поки ти не передумаєш.
Аарон Холл

@AaronHall З посилання на мову Python : " Розширений метод порівняння може повернути синглтон, NotImplementedякщо він не реалізує операцію для заданої пари аргументів. За домовленістю Falseі Trueповертаються для успішного порівняння. Однак ці методи можуть повертати будь-яке значення , тому, якщо оператор порівняння використовується в булевому контексті (наприклад, за умови заяви if), Python буде викликати bool()значення, щоб визначити, чи результат істинний чи помилковий. "
Маджієро

@AaronHall Вашого здійснення __ne__вбиває важливе математичне властивість, то симетрія від !=оператора. Цей оператор є двійковим, тому його результат повинен залежати від динамічного типу обох операндів, а не лише одного. Це правильно реалізований в мовах програмування з допомогою подвійний відправки для мови , що дозволяє множинну диспетчеризацію . У Python, який дозволяє лише одноразове відправлення, подвійне відправлення моделюється шляхом повернення NotImplementedзначення.
Маджієро

В останньому прикладі є два класи, Bякі повертають тризубну рядок для всіх перевірок на __ne__, і Aякий повертається Trueна всі перевірки на __eq__. Це патологічне протиріччя. За такої суперечності було б найкраще поставити виняток. Без знань про це B, Aми не зобов'язані поважати Bреалізацію Російської Федерації __ne__з метою симетрії. Тоді як у прикладі, як Aреалізація __ne__для мене не має значення. Будь ласка, знайдіть практичний, непатологічний випадок, щоб висловити свою думку. Я оновив свою відповідь, щоб звернутися до вас.
Зал Аарона

@AaronHall Для більш реалістичного прикладу див. Приклад SQLAlchemy, поданий @ShadowRanger. Також зауважте, що той факт, що ваша реалізація __ne__робіт у типових випадках використання не робить її правильним. Літаки Boeing 737 MAX здійснили 500 000 рейсів до катастроф…
Maggyero

-1

Якщо все __eq__, __ne__, __lt__, __ge__, __le__, і має __gt__сенс для класу, то просто реалізувати __cmp__замість цього. В іншому випадку зробіть так, як ви робите, через біт Даніель Діпаоло сказав (поки я тестував його, а не дивився;))


12
Чи не __cmp__()спеціальний метод більше не підтримується в Python 3.x , так що ви повинні звикнути до використання багатих операторів порівняння.
Дон О'Доннелл

8
Або, якщо ви знаходитесь в Python 2.7 або 3.x, декоратор functools.total_ordering також дуже зручний.
Адам Паркін,

Дякую за звіт. Хоча я зрозумів багато речей за останні півтора року. ;)
Карл Кнечтел
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.