Python多重继承方法解析顺序(MRO构建算法)

今天看了python的教学视频:
https://www.safaribooksonline.com/library/view/python-programming-language/9780134217314/PYMC_08_03.html
讲到了python中多重继承的使用,视频中用多重继承的Mixin+父类实现了装饰者模式。

参考:
https://makina-corpus.com/blog/metier/2014/python-tutorial-understanding-python-mro-class-search-path

分界

python的MRO算法有新旧两种,但并不是以python2和python3为界,具体的分隔为:在python2中如果定义类的时候没有指定父类是object,即定义为

class A:
    pass

的使用旧算法。如果指定父类为object的:

class A(object):
    pass

使用新算法,python3都是用新算法。

旧算法

先看示例,类图:

python_a.png

代码:

class A:
    def who_am_i(self):
        print("I am a A")

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

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

class D(B,C):
    def who_am_i(self):
        print("I am a D")

d1 = D()
d1.who_am_i()

毫无疑问这段代码会输出“I am a D”,稍微改一下D的定义:

class D(B,C):
     pass

这时候找到了B实现的方法所以打印出“I am a B”,如果我们把B的实现也去掉:

class B(A):
    pass

奇怪的事情发生了,输出是“I am a A”,因为旧算法的MRO推导算法很直观,它使用树状结构组织类。当函数调来到来的时候从左往右的使用深度优先遍历父类:
1,检查对象是否实现了方法
2,如果没有找到,检查首个父类是否实现了该方法
3,如果没有找到,检查本类是否还继承了其他类,是的话从其他分支向上追溯
所以对于本例,查找的顺序就是D B A C A,最后一个A不重复加入最终的顺序是D B C A

来一个例子检查:

class A1():
#      def who_am_i(self):
#          print("I am a A1")
    pass

class A2():
     def who_am_i(self):
         print("I am a A2")

class A3():
     def who_am_i(self):
         print("I am a A3")

class B(A1, A2):
#     def who_am_i(self):
#         print("I am a B")
    pass

class C(A3):
    def who_am_i(self):
        print("I am a C")

class D(B,C):
#     def who_am_i(self):
#         print("I am a D")
    pass

d1 = D()
d1.who_am_i()

类图为:

python2_mro.png

套用刚才的规则,查找方法的顺序应该是D B A1 A2 C A3,所以上述代码在python2上的输出是“I am a A2”

新算法

仍然以第一段代码作为例子,重新贴一下类图

python_a.png

新算法在构建MRO的时候第一步与旧算法一样,所以初步结果是D B A C A,然后从左到右判断每个元素是否为“好头(good head)”,不是的话从链中去掉。“好头(good head)”的定义是 - 从该元素开始到链尾,不存在任何元素是该元素的子类 - 初步结果的D, B没问题,第一个A不是好头因为C是其子类所以去掉,再往后的C A也没问题了,最终结果是D B C A,所以在python 3以及当A继承于object的时候如果D,B都不实现who_am_i则最终输出的是“I am a C”。

不可构建的MRO - 新算法补充

考虑如下蛋疼的继承结构:

Use case.png

代码:

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")

f = F()
f.who_am_i()

如果使用“新算法”,按照上述章节的分析,构建出来的MRO结果应该是F A B Y X,使用python 3跑一遍,结果得到输出:

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

这又是啥毛病呢,实际上新算法用的是线性算法记为L[]运算以及merge()操作来构造MRO,改线性算法描述为:类C的线性结果是C与merge(C的所有父类的线性结果)之和,用表达式表示为:
L[C(B1...Bn)] = C + merge(L[B1]...L[Bn], B1...Bn)
上边例子的表达式就是:
L[F(A, B)] = F + merge(L[A], L[B], A, B)
merge()运算的规则是:
1,取出merge()中第一个list的第一个元素作为head
2,如果这个head不出现在其他list的尾部,则它是一个“好头”,可以从merge()中移出来
3,否则,取出下一个list中的第一个元素作同样的判断
4,重复上述过程直至所有的类都移到merge()算子的外部则构建MRO完成
5,否则,改继承结果无法生成MRO
所以对于L[F(A, B)] = F + merge(L[A], L[B], A, B)这个表达式,L[A], L[B]可以先按照L[]算法展开为:
L[A]=L[A(X, Y)]=A+merge(L[X],L[Y],X,Y)=A+merge(X,Y,X,Y)=A,X,Y
L[B]=L[B(Y, X)]=B+merge(L[Y],L[X],Y,X)=B+merge(Y,X,Y,X)=B,Y,X
例如这里的merge(X,Y,X,Y)首先判断X,因为X不在其他list的尾部(X算是list的头部)所以先提取X出来,这时候变成了A,X+merge(Y,Y)显然Y也能直接提取出来了;merge(Y,X,Y,X)同理。
把这俩结果代回去,总式子为:
L[F(A,B)] = F + merge( (A,X,Y), (B, Y, X), A, B)
这时候先判断A,因为不在其他list的尾部,所以提出出来式子变为:
F,A + merge((X, Y), (B
, Y, X), B)
再判断第二个list的第一个元素B,也不在其他list的尾部,所以提出来:
F,A,B + merge((X
, Y), (Y, X))
接着第二轮判断第一个list的头元素X
,结果在第二个list中发现了X在尾部不能提取;再判断Y*结果发现在第一个list的尾部也不行。所以最终就遇到了不可构建MRO的情况!

结论

可见,虽然python的多重继承非常强大但是必须小心注意继承顺序(避免上述章节中提及的MRO不可构建的继承结构),与Java这种单继承语言不同 - super()总是沿着继承链向上追溯。python中方法的super()并不总是指向其声明的父类的同名方法,而是沿着MRO从左到往的传播。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容