Python中的a==b调用b.__eq__(a),子类没有重写时

20 投票
2 回答
4223 浏览
提问于 2025-04-18 14:29

在 Python 2.7.6 中,假设我有一个类定义了 __eq__ 方法,还有一个这个类的子类:

>>> class A(object):
...     def __eq__(self,other):
...         print self.__class__,other.__class__
...         return True
... 
>>> class B(A):
...     pass
... 

现在我创建了每个类的一个对象,并想比较它们:

>>> a = A()
>>> b = B()
>>> a==b

我得到的结果是:

<class '__main__.B'> <class '__main__.A'>

这表明解释器调用的是 b.__eq__(a),而不是像预期的那样调用 a.__eq__(b)

文档中提到(强调部分已加粗):

  • 对于对象 xy,首先尝试 x.__op__(y)。如果没有实现或者返回 NotImplemented,那么就尝试 y.__rop__(x)。如果这也没有实现或者返回 NotImplemented,就会抛出一个 TypeError 异常。但请看下面的例外:

  • 对于上面提到的情况的例外:如果左边的操作数是一个内置类型的实例或新式类的实例,而右边的操作数是该类型或类的一个适当子类的实例,并且重写了基类的 __rop__() 方法,那么会先尝试右边操作数的 __rop__() 方法,然后再尝试左边操作数的 __op__() 方法。

    这样做是为了让子类可以完全重写二元操作符。否则,左边操作数的 __op__() 方法总是会接受右边的操作数:当期望一个特定类的实例时,任何该类的子类的实例都是可以接受的。

由于子类 B 并没有重写 __eq__ 操作符,难道不应该调用 a.__eq__(b) 而不是 b.__eq__(a) 吗? 这是预期的行为,还是一个bug?这与我理解的文档相悖:我是不是误解了文档或遗漏了其他内容?

一些相关的问题:

  • 这个回答引用了我上面提到的文档。在那个案例中,最后的问题涉及一个内置类型的对象(1)和一个新式类的实例之间的比较。在这里,我具体比较的是一个父类的实例和一个没有重写其父类的rop() 方法的子类的实例(在这种情况下,__eq__ 同时是op()rop())。

    在这种情况下,Python 实际上确实首先调用了 b.__eq__(a),而不是 a.__eq__(b),即使类 B 并没有明确重写 A

2 个回答

15

看起来即使子类只是继承了父类的行为,它也被认为是“重写”了父类的行为。这在__eq__的情况下不太明显,因为__eq__是它自己的反射,但如果你使用不同的运算符,比如__lt____gt__,就能更清楚地看到这一点,它们是彼此的反射:

class A(object):
    def __gt__(self,other):
        print "GT", self.__class__, other.__class__

    def __lt__(self,other):
        print "LT", self.__class__, other.__class__

class B(A):
    pass

然后:

>>> A() > B()
LT <class '__main__.B'> <class '__main__.A'>

注意,A.__gt__并没有被调用;相反,调用的是B.__lt__

Python 3 的文档对此进行了说明,它用不同的更准确的说法表达了这个规则(强调部分已加粗):

如果右侧操作数的类型是左侧操作数类型的子类,并且该子类提供了这个操作的反射方法,那么在调用左侧操作数的非反射方法之前,会先调用这个方法。这种行为允许子类重写它们祖先的操作。

子类确实“提供”了反射方法,只不过是通过继承的方式提供的。如果你在子类中实际去掉反射方法的行为(通过返回 NotImplemented),那么父类的方法会被正确调用(在子类的方法之后):

class A(object):
    def __gt__(self,other):
        print "GT", self.__class__, other.__class__

    def __lt__(self,other):
        print "LT", self.__class__, other.__class__

class B(A):
    def __lt__(self, other):
        print "LT", self.__class__, other.__class__
        return NotImplemented

>>> A() > B()
LT <class '__main__.B'> <class '__main__.A'>
GT <class '__main__.A'> <class '__main__.B'>

所以基本上这似乎是一个文档错误。它应该说明子类的反射方法在比较运算符中总是优先尝试调用,而不管子类是否明确重写了父类的实现。(不过正如 Mark Dickinson 在评论中提到的,这种情况只适用于比较运算符,而不适用于像__add__/__radd__这样的数学运算符对。)

在实际操作中,这种情况不太可能影响你,因为你注意到它的唯一时机是当子类没有重写父类时。但在这种情况下,子类的行为本质上和父类是一样的,所以调用哪个并不重要(除非你在比较方法中做了一些危险的事情,比如修改对象,这种情况下你本来就应该保持警惕)。

7

下面是实现上述逻辑的代码:

Python 2.7:

/* Macro to get the tp_richcompare field of a type if defined */
#define RICHCOMPARE(t) (PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE) \
             ? (t)->tp_richcompare : NULL)

...

static PyObject *
try_rich_compare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;

    if (v->ob_type != w->ob_type &&
        PyType_IsSubtype(w->ob_type, v->ob_type) &&
        (f = RICHCOMPARE(w->ob_type)) != NULL) {
        res = (*f)(w, v, _Py_SwappedOp[op]);  // We're executing this
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(v->ob_type)) != NULL) {
        res = (*f)(v, w, op);  // Instead of this.
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(w->ob_type)) != NULL) {
        return (*f)(w, v, _Py_SwappedOp[op]);
    }
    res = Py_NotImplemented;
    Py_INCREF(res);
    return res;
}

Python 3.x:

/* Perform a rich comparison, raising TypeError when the requested comparison
   operator is not supported. */
static PyObject *
do_richcompare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;
    int checked_reverse_op = 0; 

    if (v->ob_type != w->ob_type &&
        PyType_IsSubtype(w->ob_type, v->ob_type) &&
        (f = w->ob_type->tp_richcompare) != NULL) {
        checked_reverse_op = 1; 
        res = (*f)(w, v, _Py_SwappedOp[op]);  // We're executing this
        if (res != Py_NotImplemented)
            return res; 
        Py_DECREF(res);
    }    
    if ((f = v->ob_type->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);   // Instead of this.
        if (res != Py_NotImplemented)
            return res; 
        Py_DECREF(res);
    }    
    if (!checked_reverse_op && (f = w->ob_type->tp_richcompare) != NULL) {
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res; 
        Py_DECREF(res);
    }    

这两个版本的代码很相似,唯一的不同是Python 2.7版本使用了一个叫做RICHCOMPARE的宏,它检查的是PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE,而不是ob_type->tp_richcompare != NULL

在这两个版本中,第一个if语句的结果都是true。根据文档的描述,可能会让人觉得应该是false的部分是:f = w->ob_type->tp_richcompare != NULL(对于Python 3)/ PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE。但是文档中提到,tp_richcompare是可以被子类继承的

richcmpfunc PyTypeObject.tp_richcompare

这是一个指向丰富比较函数的可选指针……

这个字段会和tp_compare、tp_hash一起被子类继承……

在2.x版本中,PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE也会返回true,因为Py_TPFLAGS_HAVE_RICHCOMPARE标志在tp_richcomparetp_cleartp_traverse都为true时也为true,而这些都是从父类继承来的。

所以,即使B没有提供自己的丰富比较方法,它仍然会返回一个非NULL的值,因为它的父类提供了这个方法。正如其他人所说,这似乎是文档中的一个错误;子类实际上并不需要重写父类的__eq__方法,只需要提供一个,即使是通过继承的方式。

撰写回答