多重继承与方法解析顺序

多重继承与方法解析顺序

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名的方法引起。这种冲突称作“菱形问题”如下图
截图

(左)说明“菱形问题”的UML类图;(右)虚线箭头是下面示例代码(代码段一)的方法解析顺序

代码段一 diamond.py: 上图中的 A,B,C和D四个类

class A:
    def ping(self):
        print('ping:', self)


class B(A):
    def pong(self):
        print('pong:', self)


class C(A):
    def pong(self):
        print('PONG:', self)


class D(B, C):
    def ping(self):
        super().ping()
        print('post-ping:', self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)

注意,B和C都实现了pong方法,二者之间唯一的区别是,C.pong 方法输出的是大写的PONG。

在D的实例上调用d.pong()方法的话,运行的是哪个pong方法呢?
在C++中程序员必须使用类名限定方法的调用来避免这种歧义。Python也能这么做,如代码段二所示

代码段二 在D实例上调用pong方法的两种方式

In [1]: from diamond import *

In [2]: d = D()

In [3]: d.pong()    # ➊
pong: <diamond.D object at 0x7fbfb4b9da58>

In [4]: C.pong(d)   # ➋
PONG: <diamond.D object at 0x7fbfb4b9da58>

In [5]:

➊ 直接调用d.pong()运行的是B类中的版本。
➋ 超类中的方法都可以直接调用,此时需要把实例作为显示参数传入。

Python 能区分d.pong() 调用的是那个方法,是因为Python会按照特定的顺序遍历继承图。这个顺序叫做方法解析顺序(Method Resolution Order, MRO)。类都有一个名为 mor 的属性,它的值是一个元组,按照这个方法解析顺序列出各个超类,从当前类一直向上,直到object类。D类的__mor__属性如下(代码段三):

代码段三

    In [5]: D.__mro__
    Out[5]: (diamond.D, diamond.B, diamond.C, diamond.A, object)

    In [6]: 

若想把方法调用委托给超类,推荐的方式是使用内置的 super() 函数。在 Python 3 中,这种方式变得更容易了,如代码段一所示。(ps:在Python 2 中,要把 D.pingpong 方法的第二行从 super().ping() 改成 super(D,self).ping()。)然而,有时可能需要绕过方法解析顺序,直接调用某个超类的方法——这样做有时更方便。例如,D.ping 方法可以这样写:

    def ping(self):
        A.ping(self) # 而不是super().ping()
        print('post-ping:', self)

注意,直接在类上调用实例方法时,必须显式传入 self 参数,因为这样访问的是未绑定方法(unbound method)。

然而,使用 super() 最安全,也不易过时。调用框架或不受自己控制的类层次结构中的方法时,尤其适合使用 super()。使用 super() 调用方法时,会遵守方法解析顺序,如代码段四所示。

代码段四 使用 super() 函数调用 ping 方法(源码在代码段一)

In [1]: from diamond import *

In [2]: d = D()    # ➊

In [6]: d.ping()
ping: <diamond.D object at 0x7fbfb4b9da58>      # ➋
post-ping: <diamond.D object at 0x7fbfb4b9da58>  # ➌

❶ D 类的 ping 方法做了两次调用。

❷ 第一个调用是 super().ping();super 函数把 ping 调用委托给 A
类;这一行由 A.ping 输出。

❸ 第二个调用是 print('post-ping:', self),输出的是这一行。

下面来看在 D 实例上调用 pingpong 方法得到的结果,如代码段五 所
示。

代码段五

In [1]: from diamond import *

In [2]: d = D()

In [7]: d.pingpong()
ping: <diamond.D object at 0x7fbfb4b9da58>  # ➊
post-ping: <diamond.D object at 0x7fbfb4b9da58>
ping: <diamond.D object at 0x7fbfb4b9da58>  # ➋
pong: <diamond.D object at 0x7fbfb4b9da58>  # ➌
pong: <diamond.D object at 0x7fbfb4b9da58>  # ➍
PONG: <diamond.D object at 0x7fbfb4b9da58>  # ➎

❶ 第一个调用是 self.ping(),运行的是 D 类的 ping 方法,输出这
一行和下一行。

❷ 第二个调用是 super().ping(),跳过 D 类的 ping 方法,找到 A 类
的 ping 方法。

❸ 第三个调用是 self.pong(),根据 mro ,找到的是 B 类实现的
pong 方法。

❹ 第四个调用是 super().pong(),也根据 mro ,找到 B 类实现
的 pong 方法。

➎ 第五个调用是 C.pong(self),忽略 mro ,找到的是 C 类实现的
pong 方法。

方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果在 diamond.py 文件(见示例 代码段一)中把 D 类声明为class D(C, B):,那么 D 类的 mro 属性就会不一样:先搜索 C 类,再搜索 B 类。

分析类时,我经常在交互式控制台中查看 mro 属性。示例 代码段六 中
是一些常用类的方法搜索顺序。

代码段六 查看几个类的 mro 属性

In [9]: bool.__mro__ # ➊  
Out[9]: (bool, int, object)

In [10]: def print_mro(cls):  # ➋
    ...:     print(", ".join(c.__name__ for c in cls.__mro__))
    ...:     

In [11]: print_mro(bool)
bool, int, object

In [13]: import numbers
    ...: 

In [14]: print_mro(numbers.Integral)  #  ➌
Integral, Rational, Real, Complex, Number, object

In [15]:  import io  # ➍

In [16]: print_mro(io.BytesIO)
    ...: 
BytesIO, _BufferedIOBase, _IOBase, object

In [17]:  print_mro(io.TextIOWrapper)
    ...: 
TextIOWrapper, _TextIOBase, _IOBase, object

❶ bool 从 int 和 object 中继承方法和属性。
❷ print_mro 函数使用更紧凑的方式显示方法解析顺序。
❸ 这些是 numbers 模块提供的几个数字抽象基类。
io 模块中有抽象基类(名称以 ...Base 后缀结尾)和具体类,如
❹ BytesIO 和 TextIOWrapper。open() 函数返回的对象属于这些类型,
具体要根据模式参数而定。

结束对方法解析顺序的讨论之前,我们来看看下图。这幅图展示了Python 标准库中 GUI 工具包 Tkinter 复杂的多重继承图。研究这幅图时,要从底部的 Text 类开始。这个类全面实现了多行可编辑文本小组件,它自身有丰富的功能,不过也从其他类继承了很多方法。左边是常规的 UML 类图。右边加入了一些箭头,表示方法解析顺序。使用示例

代码段 六 中定义的便利函数 print_mro 得到的输出如下:

In [19]: import tkinter

In [20]: print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

tk示意

(左)Tkinter 中 Text 小组件类及其超类的 UML 类图;
(右)使用虚线箭头表示 Text.__mro__

发表评论