不要混淆苹果和橙子

问题

我正在使用__eq__运算符和NotImplemented值。

我想了解当obj1.__eq__(obj2)返回NotImplementedobj2.__eq__(obj1)也返回NotImplemented时会发生什么。

根据 Why return NotImplemented instead of raising NotImplementedError 的答案以及“LiveJournal”博客中详细文章 How to override comparison operators in Python 的答案,运行时应回退到内置行为(基于==!=的标识)。

代码样例

但是,尝试下面的示例,似乎我对每对对象都有多次对__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

最佳答案

这是Python跟踪器中的issue #6970;在2.7,Python 3.0和3.1中仍未修复。

这是由于在执行两个使用__eq__方法的自定义类之间的比较时,在两个位置尝试直接比较和交换比较引起的。

丰富的比较通过 PyObject_RichCompare() function进行,它针对具有不同类型的对象(间接地)委托(delegate)给 try_rich_compare() 。在此函数中,vw是左操作数对象和右操作数对象,并且由于两者都具有__eq__方法,因此该函数同时调用v->ob_type->tp_richcompare()w->ob_type->tp_richcompare()

对于自定义类, tp_richcompare() slot定义为 slot_tp_richcompare() function,此函数再次对双方执行__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)selfother交换后被调用。

请注意,此问题仅限于不同自定义类的实例,其中两个类都定义了slot_tp_richcompare()方法。如果任何一方都没有这样的方法,__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__返回__eq__,那么您甚至可以得到六个比较:
>>> 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

为了进行优化,我们调用了第一组两个比较。当两个实例具有相同的类型时,您只需要调用NotImplemented即可,毕竟可以跳过强制(对于数字)。但是,当比较失败(返回v->tp_richcompare(v, w))时,也会尝试使用标准路径。

在Python 2中如何进行比较变得相当复杂,因为仍然必须支持较旧的NotImplemented 3向比较方法。在Python 3中,由于不再支持__cmp__,因此更容易解决该问题。因此,此修复程序从未回退到2.7。

10-08 11:23