Python “MRO三定律”——关于Python中多继承C3-MRO算法的剖析

本文将从常见的问题入手,深入刨析C3-MRO算法的历史、思想、实现

0.声明

  • 仅代表个人观点,难免有缺漏之处,欢迎指正
  • Keep it simple, stupid !
  • 本文假设您已经对Python中面向对象的基础内容有所掌握,对于细节不多做讨论
  • 本文有明确的主题,故默认顶层基类object(不涉及type有关内容)

1.关于MRO


1.0 什么是MRO?

方法解析顺序(Method Resolution Order, MRO
是在面向对象编程中,当某个实例对象应用了继承,进而引发多态特性时,编译/解释器查找并决定具体实例方法的顺序,按照标准的定义,MRO包含以下两种情况:

  • 单继承中的MRO----这种情况比较简单。
  • 多继承中的MRO----这种情况相对复杂,并且随着类继承层次的混乱,复杂程度往往超乎想象。

一般情况下所提到的MRO基本都是指复杂多继承中的MRO,本质是一个顺序,可用具体编程语言中的有序列表表示,本文同一般情况。


1.1 MRO有什么用?

  • 实现方法重载
  • 构成OOP多态
  • 保证继承有效

可以说,MRO是OOP(面向对象中)的一根顶梁柱,没了它,OOP的特性和优势都会大打折扣。

关于MRO的作用,还是有点不清晰是吗?建议你回顾一下关于OOP重载、继承、多态的内容。

2.关于C3-MRO算法


2.0 什么是C3-MRO算法?

C3 superclass linearization(C3超类线性化算法),主要用于获取在存在多继承的情况下的MRO(方法解析顺序)12。其本质是一个排序算法

1996年的OOPSLA会议上,论文"A Monotonic Superclass Linearization for Dylan"1首次提出了C3超类线性化。其后被应用于Python2.3中新式类的MRO解析。
为了凸显其算法的实际应用,便于叙述,本文中称之为C3-MRO算法。

2.1 C3-MRO算法与Python有什么关系?

Python2.2版本向2.3版本过度时34,为了贯彻OOP的语言层级设计,在已有的经典类基础上新增了新式类(Python3中摒弃了经典类,仅留新式类,可以说新式类就是Python3中当前使用的类

关于两个类型的具体内容在此不多做探究,但新式类有一个重要特性就是在未显示声明继承的情况下默认继承自object类,配合Python允许多继承的语法设计,初期为开发者们带来了不小的问题。勾出了不少隐居大佬掏出了珍藏的各种黑魔法现身江湖,但很不幸都没能获得足够的成效:

PEP 253The Python 2.3 Method Resolution Order5
[Python-Dev] perplexed by mro6

最终开发者们发现,其实早有学者研究出了合适的解决方案:1992年苹果推出了 Dylan 语言,1996年相关的论文1提出了C3算法。于是C3算法在2003年临危受命,揽下了解决Python2.3版本中新式类MRO的烂摊子,时至2020年的Python3.9.05a版本3,仍是Python中解决多继承MRO问题的核心算法

事实上,在Python中,你是可以利用cls.__mro__或者cls.mro()直接看到类或对象的MRO列表的(二者的区别在于返回的分别是元组和列表),而目前为止,它就是基于C3-MRO算法的:

class A(object):
	pass
class B(A):
	pass

print(B.__mro__)
# (<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
print(b.mro())
# [<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]

#二者的区别在于返回的分别是元组和列表

3.!!! C3-MRO算法思路剖析 !!!


本文核心部分

以下内容仅建立在作者的理解上,带有浓重个人色彩,难免缺乏客观度。但思路相对自然,希望能给您一点启发!

3.0 C3-MRO算法的具体内容3 7 4 8

我们先把思想放一放,看看C3-MRO的具体内容。

首先为了方便讨论,我们规定:

  • 类 C 的MRO亦称为C的线性化,记作 L[C] = [C1, C2, … CN]
  • L[object]=object
  • 在 L[C] = [C1, C2, … CN] 中,称首项C1为L[C]的,记作L[C]head
  • 在 L[C] = [C1, C2, … CN] 中,称L[C]的头以外的后续元素序列(可以为空)为L[C]的,记作L[C]tail
  • 在其他列表的尾中不曾出现的头,我们称之为好头,记作H
  • + 号表示列表合并

如果一个 类 C 继承自基类 B1, B2, …,BN,那么有:

L[C] = [C] + merge( L[B1], L[B2], …, L[BN], [B1, B2, …, BN] )

C3-MRO方法的主式是很清晰明了的,但其中还有一个自定运算merge待解释。

merge是一个特殊的列表合并操作,接受多个列表输入,输出为一个合并后的列表,其过程为:

① 输入中的第一个列表L[B1],取其头L[B1]head
②检查 L[B1]head 是否出现在其他列表的尾中,若未曾出现过,说明L[B1]head为好头H,将其提取至外层,然后从所有列表中删除该好头,回到步骤① 继续;若出现过,取下一个列表的头L[B2]head,从步骤②继续。

重复上述步骤,直至列表为空或者不能再找出好头。如果列表为空,则算法结束;如果列表不为空,并且无法找出可以输出的元素,那么Python会抛出异常TypeError。


让我们来实际计算一下

本例取自Wikipedia2

↓↓↓ 多继承图形 ↓↓↓

复杂多继承

↓↓↓ 具体类间关系 ↓↓↓
O=object
class A(O)
class B(O)
class C(O)
class D(O)
class E(O)
class K1(A, B, C)
class K2(D, B, E)
class K3(D, A)
class Z(K1, K2, K3)

不要急,也不要怕,这并不复杂,只是看起来比较长而已
让我们一步一步来

#我们先来试试简单的,从最顶上的O开始怎么样
#这里我们用O表示object,还记得我们的规定吗?这就是第二条
L(O)  := [O]                               


#好我们稍微提升点难度,来看看A

L[A]  := [A] + merge(L[O], [O])            #先展开主式
       = [A] + merge([O], [O])		       #接着展开右边的线性化运算L[O]=O
       = [A, O]                            #头O不在其他列表的尾中,提出O



#找到点感觉了吗?
#试试自行完成下面几个
L(B)  := [B, O]
L(C)  := [C, O]
L(D)  := [D, O]
L(E)  := [E, O]

#有没有觉得上面的计算其实都有点小题大作,
#这主要是因为截至目前都只是单继承,凭借直感观就完全足够了
#不过接下来就是这个算法展现神奇的时候了


#这里就比较复杂
#慢慢来,不要急
L(K1) := [K1] + merge(L(A), L(B), L(C), [A, B, C])            #先展开主式
       = [K1] + merge([A, O], [B, O], [C, O], [A, B, C])      #然后展开其中我们已经得出结果的线性化运算
       = [K1, A] + merge([O], [B, O], [C, O], [B, C])         #从第一个列表开始:A是个好头,把它提出来
       = [K1, A, B] + merge([O], [O], [C, O], [C])            #O不是个好头啊,没关系,从下一个列表开始,B是好头,提取出来
       = [K1, A, B, C] + merge([O], [O], [O])                 #C同上
       = [K1, A, B, C, O]                                     #这里就很明显了,都是甚至没有尾能和头比较(都是空尾),提出O


###接下来就是重复这个过程了

L(K2) := [K2] + merge(L(D), L(B), L(E), [D, B, E])
       = [K2] + merge([D, O], [B, O], [E, O], [D, B, E])
       = [K2, D] + merge([O], [B, O], [E, O], [B, E])
       = [K2, D, B] + merge([O], [O], [E, O], [E])
       = [K2, D, B, E] + merge([O], [O], [O])
       = [K2, D, B, E, O]

L(K3) := [K3] + merge(L(D), L(A), [D, A])
       = [K3] + merge([D, O], [A, O], [D, A])
       = [K3, D] + merge([O], [A, O], [A])
       = [K3, D, A] + merge([O], [O])
       = [K3, D, A, O]

L(Z)  := [Z] + merge(L(K1), L(K2), L(K3), [K1, K2, K3])
       = [Z] + merge([K1, A, B, C, O], [K2, D, B, E, O], [K3, D, A, O], [K1, K2, K3])
       = [Z, K1] + merge([A, B, C, O], [K2, D, B, E, O], [K3, D, A, O], [K2, K3])
       = [Z, K1, K2] + merge([A, B, C, O], [D, B, E, O], [K3, D, A, O], [K3])
       = [Z, K1, K2, K3] + merge([A, B, C, O], [D, B, E, O], [D, A, O])
       = [Z, K1, K2, K3, D] + merge([A, B, C, O], [B, E, O], [A, O])
       = [Z, K1, K2, K3, D, A] + merge([B, C, O], [B, E, O], [O])
       = [Z, K1, K2, K3, D, A, B] + merge([C, O], [E, O], [O])
       = [Z, K1, K2, K3, D, A, B, C] + merge([O], [E, O], [O])
       = [Z, K1, K2, K3, D, A, B, C, E] + merge([O], [O], [O])
       = [Z, K1, K2, K3, D, A, B, C, E, O]

强烈建议您阅读并尝试自行推导此例
这将有助于您理解下文


3.1 理解算法核心思想


MRO本质是一个顺序,也可以理解为Python中的一个list
C3-MRO的本质是一个排序算法

个人是从多属性排序的角度来理解C3-MRO算法的。
多属性排序中的核心问题,其实就是决定属性间的优先级

听说过“机器人三定律”吗?
如果你是第一次看到它。不妨去了解一下。作为程序员,这是我们迟早要面对的问题

个人认为,我们完全可以将C3-MRO算法的核心思想称之为“MRO三定律”

Ⅰ. MRO应保证子类在父类前。

Ⅱ. MRO应维持单调性,但不能因此违反Ⅰ。

Ⅲ. MRO应遵循局部优先性,但不能因此违反Ⅰ或 Ⅱ。

那么,“MRO三定律”是如何体现在具体算法中的呢?

请注意:事实上,思想与算法是完美糅合的,彻底的解耦是难以实现的,故作者在这里只挑选了其中易理解的部分进行说明。更多的内容留待您自行感悟。


Ⅰ. 子类在父类前

这是继承和多态的基本立足点。

例:属于OOP(面向对象编程的基本内容),在此不多做展开

这一定律在算法中主要体现在:

L[C] = [C] + merge( L[B1], L[B2], …, L[BN], [B1, B2, …, BN])
C作为子类优于C的父类,所以在算法主式中首先会将C本身抽出来,然后对父类依序递归调用

这个操作微不足道,貌似有点配不上它作为第一定律的最高身份是吗?
不,恰相反,大道至简!
要注意,算法是递归的,这一个小操作会在层层递归中一步步构建出整体的大秩序。
我们可以思考一下,递归一次又一次的调用自身后,最后一层会递归在object返回,
紧接着外面一层递归会取出当前的类(也就是object的子类),放在最前面,
再外面一层再取出当前的类(object的子类的子类),放在最前面,
再外面一层再取出当前的类(object的子类的子类),放在最前面,
···
······
递归返回与调用的顺序相反; 而每次把当前类放到列表最前面,会构造一个和遍历顺序相反的列表,所以最终生成的列表与递归调用的方向相同(反反得正)。
这个小小的操作,实际上让我们完成了从当前类沿继承结构一路向上的遍历。


L[C] = [C] + merge( L[B1], L[B2], …, L[BN], [B1, B2, …, BN])
merge中寻找好头并提取(不包括在末尾的原生父类列表中的判断)

㈠决定了我们的大方向是沿继承结构一路向上,但还存在一个问题,那就是如何在具体继承产生分岔时选择下一步的方向:
此处我们假设class C(A,B)

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值