返回介绍

12.2 多重继承和方法解析顺序

发布于 2024-02-05 21:59:47 字数 5949 浏览 0 评论 0 收藏 0

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为“菱形问题”,如图 12-1 和示例 12-4 所示。

图 12-1:(左)说明“菱形问题”的 UML 类图;(右)虚线箭头是示例 12-4 使用的方法解析顺序

示例 12-4 diamond.py:图 12-1 中的 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 也能这么做,如示例 12-5 所示。

示例 12-5 在 D 实例上调用 pong 方法的两种方式

>>> from diamond import *
>>> d = D()
>>> d.pong()  # ➊
pong: <diamond.D object at 0x10066c278>
>>> C.pong(d)  # ➋
PONG: <diamond.D object at 0x10066c278>

❶ 直接调用 d.pong() 运行的是 B 类中的版本。

❷ 超类中的方法都可以直接调用,此时要把实例作为显式参数传入。

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

>>> D.__mro__
(<class 'diamond.D'>, <class 'diamond.B'>, <class 'diamond.C'>,
<class 'diamond.A'>, <class 'object'>)

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

4在 Python 2 中,要把 D.pingpong 方法的第二行从 super().ping() 改成 super(D, self).ping()。

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

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

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

示例 12-6 使用 super() 函数调用 ping 方法(源码在示例 12-4 中)

>>> from diamond import D
>>> d = D()
>>> d.ping()  # ➊
ping: <diamond.D object at 0x10cc40630>  # ➋
post-ping: <diamond.D object at 0x10cc40630>  # ➌

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

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

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

下面来看在 D 实例上调用 pingpong 方法得到的结果,如示例 12-7 所示。

示例 12-7 pingpong 方法的 5 个调用(源码在示例 12-4 中)

>>> from diamond import D
>>> d = D()
>>> d.pingpong()
ping: <diamond.D object at 0x10bf235c0>  # ➊
post-ping: <diamond.D object at 0x10bf235c0>
ping: <diamond.D object at 0x10bf235c0>  # ➋
pong: <diamond.D object at 0x10bf235c0>  # ➌
pong: <diamond.D object at 0x10bf235c0>  # ➍
PONG: <diamond.D object at 0x10bf235c0>  # ➎

❶ 第一个调用是 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 文件(见示例 12-4)中把 D 类声明为 class D(C, B):,那么 D 类的 __mro__ 属性就会不一样:先搜索 C 类,再搜索 B 类。

分析类时,我经常在交互式控制台中查看 __mro__ 属性。示例 12-8 中是一些常用类的方法搜索顺序。

示例 12-8 查看几个类的 __mro__ 属性

>>> bool.__mro__ ➊
(<class 'bool'>, <class 'int'>, <class 'object'>)
>>> def print_mro(cls): ➋
...   print(', '.join(c.__name__ for c in cls.__mro__))
...
>>> print_mro(bool)
bool, int, object
>>> from frenchdeck2 import FrenchDeck2
>>> print_mro(FrenchDeck2) ➌
FrenchDeck2, MutableSequence, Sequence, Sized, Iterable, Container, object
>>> import numbers
>>> print_mro(numbers.Integral) ➍
Integral, Rational, Real, Complex, Number, object
>>> import io ➎
>>> print_mro(io.BytesIO)
BytesIO, _BufferedIOBase, _IOBase, object
>>> print_mro(io.TextIOWrapper)
TextIOWrapper, _TextIOBase, _IOBase, object

❶ bool 从 int 和 object 中继承方法和属性。

❷ print_mro 函数使用更紧凑的方式显示方法解析顺序。

❸ FrenchDeck2 类的祖先包含 collections.abc 模块中的几个抽象基类。

❹ 这些是 numbers 模块提供的几个数字抽象基类。

❺ io 模块中有抽象基类(名称以 ...Base 后缀结尾)和具体类,如 BytesIO 和 TextIOWrapper。open() 函数返回的对象属于这些类型,具体要根据模式参数而定。

方法解析顺序使用 C3 算法计算。Michele Simionato 的论文“The Python 2.3 Method Resolution Order”对 Python 方法解析顺序使用的 C3 算法做了权威论述。如果对方法解析顺序的细节感兴趣,可以阅读延伸阅读中给出的资料。不用过分担心,C3 算法不难理解,Simionato 写道:

……除非大量使用多重继承,或者继承关系不同寻常,否则不用了解 C3 算法,因此也不用阅读这篇论文。

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

>>> import tkinter
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object

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

下一节以真实框架为例说明多重继承的优缺点。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文