Python多重继承super()的MRO坑

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)
<ipython-input-10-d145cbf493e5> in <module>()
----> 1 obj.test()

<ipython-input-8-d5f2bb42010a> in test(self)
     13 class C(A, B):
     14     def test(self):
---> 15         super().test()
     16         print('C')
     17

<ipython-input-8-d5f2bb42010a> in test(self)
      1 class A:
      2     def test(self):
----> 3         super().test()
      4         print('A')
      5

<ipython-input-8-d5f2bb42010a> 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。 而由于AB都默认继承于object,所以后面还得再加个object。 而super(),本质上就是调用这个C.__mro__元组的下一个类。 对object来说,是没有object.test这个method的,所以会有AttributeError

解决方案竟然是abc

不对称的设计是不行的。 如果class B不写super,假如另一个类的定义写成class C(B, A),也会出现继承method未调用的问题。

这个问题的解决方案,只有在AB上面,再加一层公共基类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混到了AB之间,结果却是正常的。

>>> 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的多继承还是很方便的。

参考


相关笔记