Python中如何处理__eq__方法并按照什么顺序进行处理?

151

由于Python没有提供其比较运算符的左/右版本,它是如何决定调用哪个函数的?

class A(object):
    def __eq__(self, other):
        print "A __eq__ called"
        return self.value == other
class B(object):
    def __eq__(self, other):
        print "B __eq__ called"
        return self.value == other

>>> a = A()
>>> a.value = 3
>>> b = B()
>>> b.value = 4
>>> a == b
"A __eq__ called"
"B __eq__ called"
False

这似乎调用了两个__eq__函数。

我正在寻找官方的决策树。


@呂殿下-Luidenka 请不要编辑问题以将Python代码从2更新到3,除非该问题已标记并明确涉及Python 3。(请参阅此元帖:https://meta.stackoverflow.com/a/421314/6689725) - Z4-tier
3个回答

167

表达式a == b调用了A.__eq__,因为它存在。 它的代码包括self.value == other。 由于整数不知道如何将自己与B进行比较,Python尝试调用B.__eq__以查看它是否知道如何将自己与整数进行比较。

如果您修改您的代码以显示正在比较哪些值:

class A(object):
    def __eq__(self, other):
        print("A __eq__ called: %r == %r ?" % (self, other))
        return self.value == other
class B(object):
    def __eq__(self, other):
        print("B __eq__ called: %r == %r ?" % (self, other))
        return self.value == other

a = A()
a.value = 3
b = B()
b.value = 4
a == b

它将打印:

A __eq__ called: <__main__.A object at 0x013BA070> == <__main__.B object at 0x013BA090> ?
B __eq__ called: <__main__.B object at 0x013BA090> == 3 ?

对于那些想知道的人,在Python3中也是相关的。 - Mr.O

73
当Python2.x看到 a == b 时,它会尝试以下操作。
  • 如果 type(b) 是新式类,并且 type(b) type(a) 的子类,并且 type(b) 已经重写了 __eq__ ,则结果为 b.__eq__(a)
  • 如果 type(a) 已经重写了 __eq__ (也就是说, type(a).__eq__ 不是 object.__eq__ ),则结果为 a.__eq__(b)
  • 如果 type(b) 已经重写了 __eq__ ,则结果为 b.__eq__(a)
  • 如果以上情况都不是,则Python会寻找 __cmp__ 。 如果存在,则对象相等当且仅当它返回 zero
  • 作为最后的后备方案,Python调用 object.__eq__(a,b),当且仅当 a b 是同一个对象时,结果为 True
如果任何特殊方法返回NotImplemented,Python会像该方法不存在一样处理。
请仔细注意最后一步:如果既没有a也没有b重载==,那么a == ba is b是相同的。

来自 https://eev.ee/blog/2012/03/24/python-faq-equality/


1
嗯,看起来 Python 3 文档是错误的。请参见 http://bugs.python.org/issue4395 和补丁以获得澄清。简而言之,即使在右侧,子类仍然首先进行比较。 - max
嗨Kev,好帖子。你能解释一下第一个要点在哪里记录以及为什么设计成那样吗? - wim
1
是的,这在Python2中有文档记录吗?这是PEP吗? - Mr_and_Mrs_D
根据这个答案和相关评论,这让我比之前更加困惑了。 - Sajuuk
顺便问一下,仅在某种类型的实例上定义一个绑定方法“__eq__”是否足以覆盖“==”? - Sajuuk
为什么这个搜索 __eq__ 的过程不会递归遍历整个类层次结构呢? - Sajuuk

52

这个算法的Python 3变化/更新

How is __eq__ handled in Python and in what order?

a == b
一般来说,但并非总是如此,a == b会调用a.__eq__(b)或者type(a).__eq__(a, b)。明确地说,评估的顺序是:
  1. 如果b的类型是严格的子类(而不是相同类型)a的类型并且有一个__eq__,则调用它,如果比较被实现,则返回值。
  2. 否则,如果a__eq__,则调用它,如果比较被实现,则返回它。
  3. 否则,查看是否未调用b__eq__并且其拥有该方法,然后调用并返回它(如果比较被实现)。
  4. 最后,对于标识进行比较,与is相同的比较。
我们知道如果方法返回NotImplemented,则没有实现比较。
(在Python 2中,寻找__cmp__方法,但已弃用并在Python 3中删除。)
让B成为A的子类来测试第一个检查的行为,这表明接受的答案在这方面是错误的:
class A:
    value = 3
    def __eq__(self, other):
        print('A __eq__ called')
        return self.value == other.value

class B(A):
    value = 4
    def __eq__(self, other):
        print('B __eq__ called')
        return self.value == other.value

a, b = A(), B()
a == b

只会在返回False之前打印出B __eq__ called。请注意,我还更正了问题中的一个小错误,即将self.valueother相比较,而不是与other.value相比较 - 在此比较中,我们得到两个对象(selfother),通常是相同类型的,因为我们在这里没有进行类型检查(但它们可以是不同的类型),我们需要知道它们是否相等。我们衡量它们是否相等的标准是检查value属性,必须在两个对象上都执行。

我们如何知道这个完整的算法?

这里的其他答案似乎不完整并且过时,所以我要更新信息并向您展示如何查找此信息。

这是在C级别处理的。

我们需要查看这里的两个不同的代码片段 - 类object的默认__eq__和查找并调用__eq__方法的代码,无论它使用默认的__eq__还是自定义的__eq__

默认__eq__

relevant C api docs中查找__eq__会显示出__eq__是由tp_richcompare处理的 - 在cpython/Objects/typeobject.c中的"object"类型定义中,对于case Py_EQ:,它在object_richcompare中定义。

    case Py_EQ:
        /* Return NotImplemented instead of False, so if two
           objects are compared, both get a chance at the
           comparison.  See issue #1393. */
        res = (self == other) ? Py_True : Py_NotImplemented;
        Py_INCREF(res);
        break;

所以在这里,如果self == other,我们返回True,否则我们返回NotImplemented对象。这是任何未实现其自己的__eq__方法的object子类的默认行为。

如何调用__eq__

然后我们找到C API文档,PyObject_RichCompare函数,它调用do_richcompare
然后我们看到"object" C定义创建的tp_richcompare函数由do_richcompare调用,所以让我们更仔细地看一下这个函数。
此函数中的第一个检查是比较对象的条件:
  • 类型不相同,但是
  • 第二个类型是第一个类型的子类,并且
  • 第二个类型有一个__eq__方法,
然后交换参数并调用其他方法,如果已实现,则返回该值。 如果未实现该方法,则继续...
    if (!Py_IS_TYPE(v, Py_TYPE(w)) &&
        PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v)) &&
        (f = Py_TYPE(w)->tp_richcompare) != NULL) {
        checked_reverse_op = 1;
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);

接下来我们看看是否可以从第一个类型中查找__eq__方法并调用它。只要结果不是NotImplemented,也就是已经实现了,我们就返回它。
    if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);

如果我们没有尝试另一种类型的方法并且它存在,那么我们会尝试它,如果比较被实现,我们就返回它。

    if (!checked_reverse_op && (f = Py_TYPE(w)->tp_richcompare) != NULL) {
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }

最后,我们得到了一个备用方案,以防它未针对任一类型实现。

备用方案检查对象的身份,即它是否是相同的对象在内存中的相同位置 - 这与 self is other 的检查相同:

    /* If neither object implements it, provide a sensible default
       for == and !=, but raise an exception for ordering. */
    switch (op) {
    case Py_EQ:
        res = (v == w) ? Py_True : Py_False;
        break;

结论

在比较中,我们首先尊重比较的子类实现。

然后,如果第一个对象的实现没有被调用,我们尝试使用第一个对象的实现进行比较,然后再尝试使用第二个对象的实现。

最后,我们使用身份测试来比较相等性。


感谢您在2020年提供Python3的更新。也许值得一提的是,数学运算符具有不同的左/右处理程序,并且两者都会尝试,例如a=A(),a+1调用a.__add__,而1+a则尝试A.__radd__(如果int add无法处理A)。对于如何捕获失败的1+a并导致A.__radd__,您有什么想法吗? - P2000
“accepted answer is wrong on this count” 但是它们有 self.value == other,不像你的帖子。 - Mustafa Aydın
这是提问者的另一个错误,我默默地纠正了它,但现在我已经通过一条说明注明了我的操作。 - Russia Must Remove Putin
2
非常详细的回答。仅供记录,这是来自文档的相关部分:如果操作数是不同类型的,并且右操作数的类型是左操作数类型的直接或间接子类,则右操作数的反射方法具有优先权,否则左操作数的方法具有优先权。虚拟子类不被考虑。 - S.B

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接