Почему NotImplemented оценивается несколько раз с помощью оператора __eq__

Не смешивайте яблоки и апельсины

Проблема

Im играет с оператором __eq__ и значением NotImplemented.

Im пытается понять, что происходит, когда obj1.__eq__(obj2) возвращает NotImplemented и obj2.__eq__(obj1) также возвращает NotImplemented.

В ответ на Зачем возвращать NotImplemented вместо повышения NotImplementedError, а подробная статья Как переопределить операторы сравнения в Python в блоге LiveJournal, время выполнения должно вернуться к встроенному поведению (которое основано на идентификации для == и !=).

Пример кода

Но, попробовав пример ниже, кажется, что у меня есть несколько вызовов __eq__ для каждой пары объектов.

class Apple(object):
    def __init__(self, color):
        self.color = color

    def __repr__(self):
        return "<Apple color='{color}'>".format(color=self.color)

    def __eq__(self, other):
        if isinstance(other, Apple):
            print("{self} == {other} -> OK".format(self=self, other=other))
            return self.color == other.color
        print("{self} == {other} -> NotImplemented".format(self=self, other=other))
        return NotImplemented


class Orange(object):
    def __init__(self, usage):
        self.usage = usage

    def __repr__(self):
        return "<Orange usage='{usage}'>".format(usage=self.usage)

    def __eq__(self, other):
        if isinstance(other, Orange):
            print("{self} == {other}".format(self=self, other=other))
            return self.usage == other.usage
        print("{self} == {other} -> NotImplemented".format(self=self, other=other))
        return NotImplemented

>>> apple = Apple("red")
>>> orange = Orange("juice")

>>> apple == orange
<Apple color='red'> == <Orange usage='juice'> -> NotImplemented
<Orange usage='juice'> == <Apple color='red'> -> NotImplemented
<Orange usage='juice'> == <Apple color='red'> -> NotImplemented
<Apple color='red'> == <Orange usage='juice'> -> NotImplemented
False

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

Ожидалось, что у меня будет только:

<Apple color='red'> == <Orange usage='juice'> -> NotImplemented
<Orange usage='juice'> == <Apple color='red'> -> NotImplemented

Затем вернемся к сопоставлению идентичности id(apple) == id(orange)False.

Ответ 1

Это проблема # 6970 в трекере Python; он остается незафиксированным в версиях 2.7 и Python 3.0 и 3.1.

Это вызвано двумя местами, которые пытаются как прямое, так и замененное сравнение, когда выполняется сравнение между двумя пользовательскими классами с методами __eq__.

Богатые сравнения проходят через PyObject_RichCompare() функцию, которая для объектов с разными типами (косвенно) делегирует на try_rich_compare(). В этой функции v и w находятся объекты левого и правого операндов, и поскольку оба имеют метод __eq__, функция вызывает как v->ob_type->tp_richcompare(), так и w->ob_type->tp_richcompare().

Для пользовательских классов tp_richcompare() слот определяется как slot_tp_richcompare() функция и эта функция снова выполняет __eq__ для обеих сторон, сначала self.__eq__(self, other), затем other.__eq__(other, self).

В конце это означает, что apple.__eq__(apple, orange) и orange.__eq__(orange, apple) вызывается для первой попытки в try_rich_compare(), а затем вызывается обратное, что приводит к вызовам orange.__eq__(orange, apple) и apple.__eq__(apple, orange) как self и other меняются местами в slot_tp_richcompare().

Обратите внимание, что проблема ограничивается экземплярами разных пользовательских классов, где оба класса определяют метод __eq__. Если обе стороны не имеют такого метода __eq__ выполняется только один раз:

>>> class Pear(object):
...     def __init__(self, purpose):
...         self.purpose = purpose
...     def __repr__(self):
...         return "<Pear purpose='{purpose}'>".format(purpose=self.purpose)
...    
>>> pear = Pear("cooking")
>>> apple == pear
<Apple color='red'> == <Pear purpose='cooking'> -> NotImplemented
False
>>> pear == apple
<Apple color='red'> == <Pear purpose='cooking'> -> NotImplemented
False

Если у вас есть два экземпляра одного и того же типа, а __eq__ возвращает NotImplemented, вы даже сравните шесть:

>>> class Kumquat(object):
...     def __init__(self, variety):
...         self.variety = variety
...     def __repr__(self):
...         return "<Kumquat variety=='{variety}'>".format(variety=self.variety)
...     def __eq__(self, other):
...         # Kumquats are a weird fruit, they don't want to be compared with anything
...         print("{self} == {other} -> NotImplemented".format(self=self, other=other))
...         return NotImplemented
...
>>> Kumquat('round') == Kumquat('oval')
<Kumquat variety=='round'> == <Kumquat variety=='oval'> -> NotImplemented
<Kumquat variety=='oval'> == <Kumquat variety=='round'> -> NotImplemented
<Kumquat variety=='round'> == <Kumquat variety=='oval'> -> NotImplemented
<Kumquat variety=='oval'> == <Kumquat variety=='round'> -> NotImplemented
<Kumquat variety=='oval'> == <Kumquat variety=='round'> -> NotImplemented
<Kumquat variety=='round'> == <Kumquat variety=='oval'> -> NotImplemented
False

Первый набор из двух сравнений был вызван из попытки оптимизации; когда два экземпляра имеют один и тот же тип, вам нужно только вызвать v->tp_richcompare(v, w), и принуждения (для чисел) могут быть пропущены в конце концов. Однако, когда это сравнение терпит неудачу (возвращается NotImplemented), тогда также проверяется стандартный путь.

Сравнение сравнений в Python 2 довольно усложнилось, поскольку более старый __cmp__ метод сравнения с тремя способами все же должен был поддерживаться; в Python 3, с поддержкой __cmp__ отброшен, было легче исправить проблему. Таким образом, исправление никогда не поддерживалось до 2.7.