C3 线性化算法与 MRO之Python中的多继承


Posted in Python onOctober 05, 2021

Python 中的方法解析顺序(Method Resolution Order, MRO)定义了多继承存在时 Python 解释器查找函数解析的正确方式。当 Python 版本从 2.2 发展到 2.3 再到现在的 Python 3,MRO算法也随之发生了相应的变化。这种变化在很多时候影响了我们使用不同版本 Python 编程的过程。

什么是 MRO

MRO 全称方法解析顺序(Method Resolution Order)。它定义了 Python 中多继承存在的情况下,解释器查找函数解析的具体顺序。什么是函数解析顺序?我们首先用一个简单的例子来说明。请仔细看下面代码:

class A():
    def who_am_i(self):
        print("I am A")
        
class B(A):
    pass
        
class C(A):
    def who_am_i(self):
        print("I am C")

class D(B,C):
    pass
    
d = D()

如果我问在 Python 2 中使用 D 的实例调用 d.who_am_i(),究竟执行的是 A 中的 who_am_i() 还是 C 中的 who_am_i(),我想百分之九十以上的人都会不假思索地回答:肯定是 C 中的 who_am_i(),因为 C 是 D 的直接父类。然而,如果你把代码用 Python 2 运行一下就可以看到 d.who_am_i() 打印的是 I am A

是不是觉得很混乱很奇怪?感到奇怪就对了!!!

这个例子充分展示了 MRO 的作用:决定基类中的函数到底应该以什么样的顺序调用父类中的函数。可以明确地说,Python 发展到现在,MRO 算法已经不是一个凭借着执行结果就能猜出来的算法了。如果没有深入到 MRO 算法的细节,稍微复杂一点的继承关系和方法调用都能彻底绕晕你。

New-style Class vs. Old-style Class

在介绍不同版本的 MRO 算法之前,我们有必要简单地回顾一下 Python 中类定义方式的发展历史。尽管在 Python 3 中已经废除了老式的类定义方式和 MRO 算法,但对于仍然广泛使用的 Python 2 来说,不同的类定义方式与 MRO 算法之间具有紧密的联系。了解这一点将帮助我们从 Python 2 向 Python 3 迁移时不会出现莫名其妙的错误。

在 Python 2.1 及以前,我们定义一个类的时候往往是这个样子(我们把这种类称为 old-style class):

class A:
    def __init__(self):
        pass

Python 2.2 引入了新的模型对象(new-style class),其建议新的类型通过如下方式定义:

class A(object):
    def __init__(self):
        pass

注意后一种定义方式显示注明类 A 继承自 object。Python 2.3 及后续版本为了保持向下兼容,同时提供以上两种类定义用以区分 old-style class 和 new-style class。Python 3 则完全废弃了 old-style class 的概念,不论你通过以上哪种方式书写代码,Python 3 都将明确认为类 A 继承自 object。这里我们只是引入 old-style 和 new-style 的概念,如果你对他们的区别感兴趣,可以自行看 stackoverflow 上有关该问题的解释

理解 old-style class 的 MRO

我们使用前文中的类继承关系来介绍 Python 2 中针对 old-style class 的 MRO 算法。如果你在前面执行过那段代码,你可以看到调用 d.who_am_i() 打印的应该是 I am A。为什么 Python 2 的解释器在确定 D 中的函数调用时要先搜索 A 而不是先搜索 D 的直接父类 C 呢?

这是由于 Python 2 对于 old-style class 使用了非常简单的基于深度优先遍历的 MRO 算法(关于深度优先遍历,我想大家肯定都不陌生)。当一个类继承自多个类时,Python 2 按照从左到右的顺序深度遍历类的继承图,从而确定类中函数的调用顺序。这个过程具体如下:

  • 检查当前的类里面是否有该函数,如果有则直接调用。
  • 检查当前类的第一个父类里面是否有该函数,如果没有则检查父类的第一个父类是否有该函数,以此递归深度遍历。
  • 如果没有则回溯一层,检查下一个父类里面是否有该函数并按照 2 中的方式递归。

上面的过程与标准的深度优先遍历只有一点细微的差别:步骤 2 总是按照继承列表中类的先后顺序来选择分支的遍历顺序。具体来说,类 D 的继承列表中类顺序为 B, C,因此,类 D 按照先遍历 B 分支再遍历 C 分支的顺序来确定 MRO。

我们继续用第一个例子中的函数继承图来说明这个过程:

C3 线性化算法与 MRO之Python中的多继承

按照上述深度递归的方式,函数 d.who_am_i() 调用的搜索顺序是 D, B, A, C, A。由于一个类不能两次出现,因此在搜索路径中去除掉重复出现的 A,得到最终的方法解析顺序是 D, B, A, C。这样一来你就明白了为什么 d.who_am_i() 打印的是 I am A 了。

在 Python 2 中,我们可以通过如下方式来查看 old-style class 的 MRO:

>>> import inspect
>>> inspect.getmro(D)

理解 new-style class 的 MRO

从上面的结果可以看到,使用深度优先遍历的查找算法并不合理。因此,Python 3 以及 Python 2 针对 new-style class 采用了新的 MRO 算法。如果你使用 Python 3 重新运行一遍上述脚本,你就可以看到函数 d.who_am_i() 的打印结果是 I am C

>>> d.who_am_i()
I am C
>>> D.__mro__
(<class 'test.D'>, <class 'test.B'>, <class 'test.C'>, <class 'test.A'>, <class 'object'>)

新算法与基于深度遍历的算法类似,但是不同在于新算法会对深度优先遍历得到的搜索路径进行额外的检查。其从左到右扫描得到的搜索路径,对于每一个节点解释器都会判断该节点是不是好的节点。如果不是好的节点,那么将其从当前的搜索路径中移除。

那么问题在于,什么是一个好的节点?我们说 N 是一个好的节点当且仅当搜索路径中 N 之后的节点都不继承自 N。我们还以上述的类继承图为例,按照深度优先遍历得到类 D 中函数的搜索路径 D, B, A, C, A。之后 Python 解释器从左向右检查时发现第三个节点 A 不是一个好的节点,因为 A 之后的节点 C 继承自 A。因此其将 A 从搜索路径中移除,然后得到最后的调用顺序 D, B, C, A。

采用上述算法,D 中的函数调用将优先查找其直接父类 B 和 C 中的相应函数。

C3线性化算法

上一小结我们从直观上概述了针对 new-style class 的 MRO 算法过程。事实上这个算法有一个明确的名字 C3 linearization。下面我们给出其形式化的计算过程。

C3 线性化算法与 MRO之Python中的多继承

上面的过程看起来好像很复杂,我们用一个例子来具体执行一下,你就会觉得其实还是挺简单的。假设我们有如下的一个类继承关系:

class X():
    def who_am_i(self):
        print("I am a X")
        
class Y():
    def who_am_i(self):
        print("I am a Y")
        
class A(X, Y):
    def who_am_i(self):
        print("I am a A")
        
class B(Y, X):
     def who_am_i(self):
         print("I am a B")
         
class F(A, B):
    def who_am_i(self):
        print("I am a F")

C3 线性化算法与 MRO之Python中的多继承

Traceback (most recent call last):
  File "test.py", line 17, in <module>
    class F(A, B):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y

参考文献 Python Tutorial: Understanding Python MRO - Class search path The Python 2.3 Method Resolution Order C3 linearization

到此这篇关于C3 线性化算法与 MRO之Python中的多继承的文章就介绍到这了,更多相关Python多继承内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Python实现查找系统盘中需要找的字符
Jul 14 Python
Django小白教程之Django用户注册与登录
Apr 22 Python
纯python实现机器学习之kNN算法示例
Mar 01 Python
python如何在列表、字典中筛选数据
Mar 19 Python
Python基于matplotlib画箱体图检验异常值操作示例【附xls数据文件下载】
Jan 07 Python
selenium+python环境配置教程详解
May 28 Python
详解python调用cmd命令三种方法
Jul 08 Python
python求加权平均值的实例(附纯python写法)
Aug 22 Python
详解mac python+selenium+Chrome 简单案例
Nov 08 Python
使用python实现时间序列白噪声检验方式
Jun 03 Python
python调用私有属性的方法总结
Jul 24 Python
Pandas 数据编码的十种方法
Apr 20 Python
Python编程super应用场景及示例解析
Python编程源码报错解决方法总结经验分享
Oct 05 #Python
Python编程根据字典列表相同键的值进行合并
Oct 05 #Python
python编程简单几行代码实现视频转换Gif示例
用 Python 定义 Schema 并生成 Parquet 文件详情
Sep 25 #Python
使用pipenv管理python虚拟环境的全过程
Sep 25 #Python
Django实现WebSocket在线聊天室功能(channels库)
Sep 25 #Python
You might like
解决FastCGI 进程超过了配置的活动超时时限的问题
2013/07/03 PHP
javascript实现的使用方向键控制光标在table单元格中切换
2010/11/17 Javascript
jquery中获取id值方法小结
2013/09/22 Javascript
jquery操作checkbox示例分享
2014/07/21 Javascript
jQuery中prepend()方法用法实例
2014/12/25 Javascript
Adapter适配器模式在JavaScript设计模式编程中的运用分析
2016/05/18 Javascript
使用RequireJS库加载JavaScript模块的实例教程
2016/06/06 Javascript
jQuery实现分页功能(含ajax请求、后台数据、附完整demo)
2017/04/03 jQuery
jsonp跨域请求详解
2017/07/13 Javascript
JS实现用特殊符号替换字符串的中间部分区域的实例代码
2018/07/24 Javascript
Vue CLI 3搭建vue+vuex最全分析(推荐)
2018/09/27 Javascript
详解Vue组件插槽的使用以及调用组件内的方法
2018/11/13 Javascript
node.js实现带进度条的多文件上传
2020/03/27 Javascript
vue实现购物车加减
2020/05/30 Javascript
[00:47]TI7不朽珍藏III——沙王不朽展示
2017/07/15 DOTA
[44:51]2018DOTA2亚洲邀请赛 4.4 淘汰赛 VP vs Liquid 第二场
2018/04/05 DOTA
rhythmbox中文名乱码问题解决方法
2008/09/06 Python
自己使用总结Python程序代码片段
2015/06/02 Python
Python实现的双色球生成功能示例
2017/12/18 Python
Python内置函数reversed()用法分析
2018/03/20 Python
Python实现的查询mysql数据库并通过邮件发送信息功能
2018/05/17 Python
使用python爬取B站千万级数据
2018/06/08 Python
Python使用random.shuffle()打乱列表顺序的方法
2018/11/08 Python
Pandas DataFrame 取一行数据会得到Series的方法
2018/11/10 Python
python基础梳理(一)(推荐)
2019/04/06 Python
Pandas_cum累积计算和rolling滚动计算的用法详解
2019/07/04 Python
python监控进程状态,记录重启时间及进程号的实例
2019/07/15 Python
python如果快速判断数字奇数偶数
2019/11/13 Python
Python3实现将一维数组按标准长度分隔为二维数组
2019/11/29 Python
python简单实现9宫格图片实例
2020/09/03 Python
详解如何用HTML5 Canvas API控制图片的缩放变换
2016/03/22 HTML / CSS
Ruby如何定义一个类
2012/10/08 面试题
专营店会计助理岗位职责
2013/11/29 职场文书
2015年大学元旦晚会活动策划书
2014/12/09 职场文书
2016年共产党员个人承诺书
2016/03/24 职场文书
AudioContext 实现音频可视化(web技术分享)
2022/02/24 Javascript