Python多重继承super()的MRO坑

标签:

Python

Python的面向对象类继承方面,采用了类似C++多重继承的方式。

而为了避免多重继承带来的菱形继承问题,Python对公共祖先的method实现了只调用一次。

但这也带来了一个问题,如何确定复杂继承关系中的method调用顺序,比如__init__的调用顺序。

为了确定调用复写函数的顺序,Python采用MRO(Method Resolution Order)的方式。

Python子类的method中调用super(),可以根据继承顺序,把对应父类的method依次调用一次。

由于所有的类都继承于object,所以这里就有个坑。

为什么少个B? 

class A:
def __init__(self):
print('A')
self.a = 1
class B:
def __init__(self):
print('B')
self.b = 2
class C(A, B):
def __init__(self):
super().__init__()
print('C')
在执行以上定义后,执行C的构造会有以下结果:
>>> obj = C()
A
C
>>> print(obj.a)
1
>>> print(obj.b)
...
AttributeError: 'C' object has no attribute 'b'
这就奇怪了。我凭本事写的多继承,凭什么丢了个B?
因为少了super() ¶
对上面的代码略作修改,添加两行super()。
class A:
def __init__(self):
super().__init__()
print('A')
self.a = 1
class B:
def __init__(self):
super().__init__()
print('B')
self.b = 2
class C(A, B):
def __init__(self):
super().__init__()
print('C')
接下来的执行结果,就比较符合预期。
>>> obj = C()
B
A
C
>>> print(obj.a)
1
>>> print(obj.b)
2
可见,super()是Python实现继承的关键。
到目前为止,仍然是容易理解的,属于正常范畴。
test方法哪去了? ¶
class A:
def test(self):
print('A')
class B:
def test(self):
print('B')
class C(A, B):
def test(self):
super().test()
print('C')
接下来的执行结果,虽然不是真正想要的,但由于有前面的经验,还算符合预期。
>>> obj = C()
>>> obj.test()
A
C
修改代码,增加super():
class A:
def test(self):
super().test()
print('A')
class B:
def test(self):
super().test()
print('B')
class C(A, B):
def test(self):
super().test()
print('C')
再执行,竟然挂了!
>>> obj = C()
>>> obj.test()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
 in ()
----> 1 obj.test()
 in test(self)
13 class C(A, B):
14 def test(self):
---> 15 super().test()
16 print('C')
17
 in test(self)
1 class A:
2 def test(self):
----> 3 super().test()
4 print('A')
5
 in test(self)
7 class B:
8 def test(self):
----> 9 super().test()
10 print('B')
11
AttributeError: 'super' object has no attribute 'test'
如果要得到理想情况,需要删除B的那一行super()。
class A:
def test(self):
super().test()
print('A')
class B:
def test(self):
print('B')
class C(A, B):
def test(self):
super().test()
print('C')
结果虽然符合预期,但代码却非常诡异。
丢失了对称性,完全没有美感。
>>> obj = C()
>>> obj.test()
B
A
C
原理 ¶
>>> import inspect
>>> inspect.getmro(C) # or C.__mro__
(__main__.C, __main__.A, __main__.B, object)
根据class C(A, B)的定义方式,C的MRO是CAB。
而由于A和B都默认继承于object,所以后面还得再加个object。
而super(),本质上就是调用这个C.__mro__元组的下一个类。
对object来说,是没有object.test这个method的,所以会有AttributeError。
解决方案竟然是abc ¶
不对称的设计是不行的。
如果class B不写super,假如另一个类的定义写成class C(B, A),也会出现继承method未调用的问题。
这个问题的解决方案,只有在A和B上面,再加一层公共基类D,让D包含这个test方法。
一般会把这个D设计为抽象类。
import abc
class D(abc.ABC):
@abc.abstractmethod
def test(self):
pass
class A:
def test(self):
super().test()
print('A')
class B:
def test(self):
super().test()
print('B')
class C(A, B):
def test(self):
super().test()
print('C')
执行结果:
>>> obj = C()
>>> obj.test()
B
A
C
>>> C.__mro__
(__main__.C, __main__.A, __main__.B, __main__.D, abc.ABC, object)
如果再加一个普通类 ¶
另外,在非object的类中,反而没有问题。
class E:
pass
class C(A, E, B):
def test(self):
super().test()
print('C')
这里特地把E混到了A和B之间,结果却是正常的。
>>> obj = C()
>>> obj.test()
B
A
C
>>> C.__mro__
(__main__.C, __main__.A, __main__.E, __main__.B, __main__.D, abc.ABC, object)

可见,object是一个特例。

结论 ¶

Python里的super,要配合abc一起使用。否则,除了object自带的方法(协议),都会出问题。

对于普通类,反而不用担心这个问题。不包含同名method的类,不会被调用,也不会报错或截断。

这个坑,真是藏得好深。

总之,了解这个特性后,Python的多继承还是很方便的。

参考 ¶