Python中的a==b调用b.__eq__(a),子类没有重写时
在 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)
。
文档中提到(强调部分已加粗):
对于对象
x
和y
,首先尝试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 个回答
看起来即使子类只是继承了父类的行为,它也被认为是“重写”了父类的行为。这在__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__
这样的数学运算符对。)
在实际操作中,这种情况不太可能影响你,因为你注意到它的唯一时机是当子类没有重写父类时。但在这种情况下,子类的行为本质上和父类是一样的,所以调用哪个并不重要(除非你在比较方法中做了一些危险的事情,比如修改对象,这种情况下你本来就应该保持警惕)。
下面是实现上述逻辑的代码:
/* 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;
}
/* 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_richcompare
、tp_clear
和tp_traverse
都为true时也为true,而这些都是从父类继承来的。
所以,即使B
没有提供自己的丰富比较方法,它仍然会返回一个非NULL的值,因为它的父类提供了这个方法。正如其他人所说,这似乎是文档中的一个错误;子类实际上并不需要重写父类的__eq__
方法,只需要提供一个,即使是通过继承的方式。