python中的super函数及MRO

版权声明:访问者可将本人原创或翻译内容或服务用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯本人的合法权利。除此以外,将本人原创或翻译内容用于其他用途时,须征得本人的同意。 https://blog.csdn.net/m0_38063172/article/details/82250865

super() 函数是用于调用父类(超类)的一个方法。
super是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。
此篇博客所用解释器为python3.x,不保证在其他版本解释器下得到相同结果,也不打算讨论早期python版本的mro机制。

调用父类(超类)方法的两种方式:

类名.方法名
这种方式简单易懂,但是无法处理多继承的问题,多继承是python面向对象技术的一项重要特性,为了利用好python的多继承特性,我们应该找到更适合的方式来调用父类方法。

super函数.方法名
super函数调用父类方法有两种形式,一种是super().父类方法,一种是super(C,obj).父类方法。其中第一种形式不用指定父类名和对应的实例化对象,使用起来更加灵活,后一种形式更加具体,所以也可以在代码的其他地方使用。因为通过super函数调用时没有类名,所以只能用于实例化方法。下面我们看一下代码演示:

class A:
    def go(self):
        print('go A go')

class B:
    def go(self):
        print('go B go')

class C(A):
    def go(self):
        super().go()
        print('go C go')

class D(B):
    def go(self):
        super().go()
        print('go D go')

class E(A):
    def go(self):
        super().go()
        print('go E go')

class F(C,E):
    def go(self):
        super().go()
        print('go F go')

class G(F,D):
    def go(self):
        super().go()
        print('go G go')

a = A()
b = B()
c = C()
d = D()
e = E()
f = F()
g = G()

让我们看看调用各个类的实例化对象的go方法分别会输出什么:

In [33]: a.go()
go A go

In [34]: b.go()
go B go

In [35]: c.go()
go A go
go C go

In [36]: d.go()
go B go
go D go

In [37]: e.go()
go A go
go E go

目前为止我们对输出的结果都能猜得到,再看最后两个:

In [38]: f.go()
go A go
go E go
go C go
go F go

In [39]: g.go()
go A go
go E go
go C go
go F go
go G go

哎,我们还是再看点简单的吧:

In [40]: super(G,g).go()
go A go
go E go
go C go
go F go

这个例子我们可以看到super函数的另外一种形式的使用。

python中的MRO和C3算法

MRO就是类的方法解析顺序表, 其实也就是继承父类方法时的顺序表。

我们可以先看一下相关的两个概念

二义性:

python支持多继承,多继承的语言往往会遇到以下两类二义性的问题:

1. 有两个基类A和B,A和B都定义了方法f(),C继承A和B,那么调用C的f()方法时会出现不确定。
2. 有一个基类A,定义了方法f(),B类和C类继承了A类(的f()方法),D类继承了B和C类,那么出现一个问题,D不知道应该继承B的f()方法还是C的f()方法。

拓扑排序:

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑排序(TopologicalOrder)的序列,简称拓扑序列。

拓扑排序的实现步骤:

循环执行以下两步,直到不存在入度为0的顶点为止

1. 选择一个入度为0的顶点并输出之
2. 从网中删除此顶点及所有出边。

python中计算MRO的C3算法就利用了拓扑排序。我们可以画出图来看看类F和G中的继承关系及MRO是怎么计算出来的。
这里写图片描述
上图为F的继承关系图,F为最终子类,我们可以分析一下C3算法中是怎么处理F的继承关系进而算出MRO的。
首先找到入度为0的点,这里是F,找到后在图中删除F及其对应的边,这时入度为0的点有两个,C和E,因为C的继承顺序在E之前,所以我们先处理C,到这里为止继承顺序是FC,删除掉C和它的边之后,入度为0的点只有E了,到这里为止继承顺序是FCE,删除掉E和它的边之后入度为0的点就剩A了,删除A和它的所有边,到这里为止继承顺序是FCEA,现在入度为0的点是Object,删除Object和它的所有边。继承图上面再没有节点了,所以继承顺序的搜索完成。最终的继承顺序是F->C->E->A->Object。

再来分析一下G的继承顺序:
这里写图片描述
首先找到入度为0的点,这里是G,删除G和它的所有边之后,入度为0的点为F和D,因为定义G时我们写的是G(F,D),F在D之前,所以先处理F,删除F和它的所有边,到这里继承顺序是GF,这时入度为0的点为C,E,D,我们先处理C,删除C和它的所有边,到这里继承顺序是GFC,这时入度为0的点为E,D,我们先处理E(写到这里我突然意识到前面应该也讲一讲广度优先搜索和深度优先搜索算法的,和这里的算法有不可分割的联系。怪不得别人要写,有点蠢了。请读者自行学习),删除E和它的所有边,到这里继承顺序是GFCE,这时入度为0的点是A和D,先处理A,到这里继承顺序是GFCEA,这时入度为0的点是D,我们删除D和它的所有边,到这里继承顺序是GFCEAD,现在入度为0的点是B,删除B和它的所有边,到这里继承顺序是GFCEADB,现在入度为0的点是Object,删除Object和它的所有边。继承图上面再没有节点了,所以继承顺序的搜索完成。最终的继承顺序是G->F->C->E->A->D->B->Object。

我们可以看到,C3算法其实可以说是深度优先搜索算法的变种,它的向深度搜索是有条件的,那就是某个点u向它可以直接到达的点v搜索时v的入度应该为0。

知道了G的继承顺序之后我们分析一下g.go()的输出结果。
调用g中的go函数时先调用f中的go函数,f中的go函数又去调用c中的go函数,c中的go函数又去调用e中的go函数,e中的go函数去调用a中的go函数,因为a中的go函数没有super函数,所以函数调用栈结束,开始从上往下输出结果。

类中的mro属性

python的类中定义了mro函数,可以查看类的mro,需要通过类名来调用。我们可以查看F类和G类的mro:

In [41]: print(F.mro())
[<class '__main__.F'>, <class '__main__.C'>, <class '__main__.E'>, <class '__main__.A'>, <class 'object'>]

In [42]: print(G.mro())
[<class '__main__.G'>, <class '__main__.F'>, <class '__main__.C'>, <class '__main__.E'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>]
参考链接

python中的MRO与多继承

感谢上面那篇博客的作者,还有其他未列出的参考链接,同样感谢那些作者的付出和他们的文章对我的启发。

猜你喜欢

转载自blog.csdn.net/m0_38063172/article/details/82250865