详解python的super()的作用和原理
Python中对象方法的定义很怪异,第一个参数一般都命名为self(相当于其它语言的this,比如:C#),用于传递对象本身,而在调用的时候则不
必显式传递,系统会自动传递。
今天我们介绍的主角是super(), 在类的继承里面super()非常常用, 它解决了子类调用父类方法的一些问题, 父类多次被调用时只执行一次, 优化了执行逻辑,下面我们就来详细看一下。
举一个例子:
1. class Foo:
2. def bar(self, message):
3. print(message)
4. >>> Foo().bar("Hello, Python.")
5. Hello, Python.
当存在继承关系的时候,有时候需要在子类中调用父类的方法,此时最简单的方法是把对象调用转换成类调用,需要注意的是这时self参数需要显式传递,例如:
1. class FooParent:
2. def bar(self, message):
3. print(message)
4. class FooChild(FooParent):
5. def bar(self, message):
6. FooParent.bar(self, message)
7. >>> FooChild().bar("Hello, Python.")
8. Hello, Python.
这样做有一些缺点,比如说如果修改了父类名称,那么在子类中会涉及多处修改,另外,Python是允许多继承的语言,如上所示的方法在多继承时就需要重复写多次,显得累赘。为了解决这些问题,Python引入了super()机制,例子代码如下:
1. class FooParent:
2. def bar(self, message):
3. print(message)
4. class FooChild(FooParent):
5. def bar(self, message):
6. super(FooChild, self).bar(message)
7.
8. >>> FooChild().bar("Hello, Python.")
9. Hello, Python
表面上看 super(FooChild, self).bar(message)方法和FooParent.bar(self, message)方法的结果是一致的,实际上这两种方法的内部处理机制大大不同,当涉及多继承情况时,就会表现出明显的差异来,直接给例子:
代码一:
1. class A:
2. def __init__(self):
3. print("Enter A")
4. print("Leave A")
5. class B(A):
6. def __init__(self):
7. print("Enter B")
8. A.__init__(self)
9. print("Leave B")
10. class C(A):
11. def __init__(self):
12. print("Enter C")
13. A.__init__(self)
14. print("Leave C")
15. class D(A):
16. def __init__(self):
17. print("Enter D")
18. A.__init__(self)
19. print("Leave D")
20. class E(B, C, D):
21. def __init__(self):
22. print("Enter E")
23. B.__init__(self)
24. C.__init__(self)
25. D.__init__(self)
26. print("Leave E")
27.
28. E()
结果:
Enter E
Enter B
Enter A
Leave A
Leave B
Enter C
Enter A
Leave A
Leave C
Enter D
Enter A
Leave A
Leave D
Leave E
执行顺序很好理解,唯一需要注意的是公共父类A被执行了多次。
代码二:
1. class A:
2. def __init__(self):
3. print("Enter A")
4. print("Leave A")
5. class B(A):
6. def __init__(self):
7. print("Enter B")
8. super(B, self).__init__()
9. print("Leave B")
10. class C(A):
11. def __init__(self):
12. print("Enter C")
13. super(C, self).__init__()
14. print("Leave C")
15. class D(A):
16. def __init__(self):
17. print("Enter D")
18. super(D, self).__init__()
19. print("Leave D")
20. class E(B, C, D):
21. def __init__(self):
22. print("Enter E")
23. super(E, self).__init__()
24. print("Leave E")
25.
26.E()
结果:
Enter E
Enter B
Enter C
Enter D
Enter A
Leave A
Leave D
Leave C
Leave B
Leave E
在super机制里可以保证公共父类仅被执行一次,至于执行的顺序,是按照MRO(Method Resolution Order)
深入 super()
看了上面的使用,你可能会觉得 super 的使用很简单,无非就是获取了父类,并调用父类的方法。其实,在上面的情况下,super 获得的类刚好是父类,但在其他情况就不一定了, super 其实和父类没有实质性的关联 。
让我们看一个稍微复杂的例子,涉及到多重继承,代码如下:
class Base (object) :
def __init__ (self) :
print "enter Base"
print "leave Base"
class A (Base) :
def __init__ (self) :
print "enter A"
super(A, self).__init__()
print "leave A"
class B (Base) :
def __init__ (self) :
print "enter B"
super(B, self).__init__()
print "leave B"
class C (A, B) :
def __init__ (self) :
print "enter C"
super(C, self).__init__()
print "leave C"
其中,Base 是父类,A, B 继承自 Base, C 继承自 A, B,它们的继承关系如下:
现在,让我们看一下使用:
如果你认为 super 代表『调用父类的方法』,那你很可能会疑惑为什么 enter A 的下一句不是 enter Base 而是 enter B。原因是, super 和父类没有实质性的关联 ,现在让我们搞清 super 是怎么运作的。
MRO 列表
事实上,对于你定义的每一个类,Python 会计算出一个 方法解析顺序(Method Resolution Order, MRO)列表 , 它代表了类继承的顺序 ,我们可以使用下面的方式获得某个类的 MRO 列表:
>>> C.mro() # or C.__mro__ or C().__class__.mro()
[__main__.C, __main__.A, __main__.B, __main__.Base, object]
那这个 MRO 列表的顺序是怎么定的呢,它是通过一个 C3 线性化算法 来实现的,这里我们就不去深究这个算法了,感兴趣的读者可以自己去了解一下,总的来说,一个类的 MRO 列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:
- 子类永远在父类前面
- 如果有多个父类,会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
super 原理
super 的工作原理如下:
def super (cls, inst) :
mro = inst.__class__.mro()
return mro[mro.index(cls) + 1 ]
其中,cls 代表类,inst 代表实例,上面的代码做了两件事:
- 获取 inst 的 MRO 列表
- 查找 cls 在当前 MRO 列表中的 index, 并返回它的下一个类,即 mro[index + 1]
当你使用 super(cls, inst) 时,Python 会在 inst 的 MRO 列表上搜索 cls 的下一个类。
现在,让我们回到前面的例子。
首先看类 C 的 __init__ 方法:
super (C, self) .__init__()
这里的 self 是当前 C 的实例,self.__class__.mro() 结果是:
[__main__ .C , __main__ .A , __main__ .B , __main__ .Base , object]
可以看到,C 的下一个类是 A,于是,跳到了 A 的 __init__ ,这时会打印出 enter A,并执行下面一行代码:
super(A, self).__init__()
注意,这里的 self 也是当前 C 的实例,MRO 列表跟上面是一样的,搜索 A 在 MRO 中的下一个类,发现是 B,于是,跳到了 B 的 __init__ ,这时会打印出 enter B,而不是 enter Base。
整个过程还是比较清晰的,关键是要理解 super 的工作方式,而不是想当然地认为 super 调用了父类的方法。
小结
- 事实上,super 和父类没有实质性的关联。
- super(cls, inst) 获得的是 cls 在 inst 的 MRO 列表中的下一个类。
python 多重继承的方法解析顺序
1. class A:
2. def say(self):
3. print("A Hello:", self)
4.
5. class B(A):
6. def eat(self):
7. print("B Eating:", self)
8.
9. class C(A):
10. def eat(self):
11. print("C Eating:", self)
12.
13. class D(B, C):
14. def say(self):
15. super().say()
16. print("D Hello:", self)
17. def dinner(self):
18. self.say()
19. super().say()
20. self.eat()
21. super().eat()
22. C.eat(self)
这里B和C都实现了eat方法,
在 D 的实例上调用 d.eat() 方法的话, 运行的是哪个 eat 方法呢?
1. >>> d = D()
2. >>> d.eat()
3. B Eating: <__main__.D object at 0x7fb90c627f60>
4. >>> C.eat(d)
5. C Eating: <__main__.D object at 0x7fb90c627f60>
6. 超类中的方法都可以直接调用, 此时要把实例作为显式参数传入
Python 能区分 d.eat() 调用的是哪个方法, 是因为 Python 会按照特定的顺序遍历继承图。 这个顺序叫方法解析顺序( Method Resolution Order, MRO)。 类都有一个名为 __mro__ 的属性, 它的值是一个元组, 按照方法解析顺序列出各个超类, 从当前类一直向上, 直到object 类。 D 类的 __mro__ 属性如下 :
1. >>> D.__mro__
2. (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>)
1. >>> d = D()
2. >>> d.dinner()
3. A Hello: <__main__.D object at 0x7fb90bd7eb70>
4. D Hello: <__main__.D object at 0x7fb90bd7eb70>
5. A Hello: <__main__.D object at 0x7fb90bd7eb70>
6. B Eating: <__main__.D object at 0x7fb90bd7eb70>
7. B Eating: <__main__.D object at 0x7fb90bd7eb70>
8. C Eating: <__main__.D object at 0x7fb90bd7eb70>
9. 第一个self.say(),运行A类的say()再print出自己的第二行信息
10. 第二个super().say(),运行A类的say()
11. 第三个self.eat(),根据 __mro__ , 找到的是 B 类实现的eat方法
12. 第四个super().eat(),根据 __mro__ , 找到的是 B 类实现的eat方法
13. 第五个C.eat(self)忽略 mro , 找到的是 C 类实现的eat方法
Python中MRO算法
MRO(Method Resolution Order):方法解析顺序 。
Python语言包含了很多优秀的特性,其中多重继承就是其中之一,但是多重继承会引发很多问题,比如二义性,Python中一切皆引用,这使得他不会像C++一样使用虚基类处理基类对象重复的问题,但是如果父类存在同名函数的时候还是会产生二义性,Python中处理这种问题的方法就是MRO。
【历史中的MRO】
如果不想了解历史,只想知道现在的MRO可以直接看最后的C3算法,不过C3所解决的问题都是历史遗留问题,了解问题,才能解决问题,建议先看历史中MRO的演化。
Python2.2以前的版本:金典类(classic class)时代
金典类是一种没有继承的类,实例类型都是type类型,如果经典类被作为父类,子类调用父类的构造函数时会出错。
这时MRO的方法为DFS(深度优先搜索(子节点顺序:从左到右))。
1 2 3 | Class A: # 是没有继承任何父类的 def __init__ (self) : print "这是金典类" |
inspect.getmro(A)可以查看金典类的MRO顺序
import inspect
class D:
pass
class C(D):
pass
class B(D):
pass
class A(B, C):
pass
if __name__ == '__main__':
print inspect.getmro(A)
>> (<class __main__.A at 0x10e0e5530>, <class __main__.B at 0x10e0e54c8>, <class __main__.D at 0x10e0e53f8>, <class __main__.C at 0x10e0e5460>)
RO的DFS顺序如下图:
两种继承模式在DFS下的优缺点。
第一种,我称为正常继承模式,两个互不相关的类的多继承,这种情况DFS顺序正常,不会引起任何问题;
第二种,棱形继承模式,存在公共父类(D)的多继承(有种D字一族的感觉),这种情况下DFS必定经过公共父类(D),这时候想想,如果这个公共父类(D)有一些初始化属性或者方法,但是子类(C)又重写了这些属性或者方法,那么按照DFS顺序必定是会先找到D的属性或方法,那么C的属性或者方法将永远访问不到,导致C只能继承无法重写(override)。这也就是为什么新式类不使用DFS的原因,因为他们都有一个公共的祖先object。
Python2.2版本:新式类(new-style class)诞生
为了使类和内置类型更加统一,引入了新式类。新式类的每个类都继承于一个基类,可以是自定义类或者其它类,默认承于object。子类可以调用父类的构造函数。
这时有两种MRO的方法
1. 如果是金典类MRO为DFS(深度优先搜索(子节点顺序:从左到右))。
2. 如果是新式类MRO为BFS(广度优先搜索(子节点顺序:从左到右))。
1 2 3 | Class A(object): # 继承于object def __init__ (self) : print "这是新式类" |
A .__mro__ 可以查看新式类的顺序
MRO的BFS顺序如下图:
两种继承模式在BFS下的优缺点。
第一种,正常继承模式,看起来正常,不过实际上感觉很别扭,比如B明明继承了D的某个属性(假设为foo),C中也实现了这个属性foo,那么BFS明明先访问B然后再去访问C,但是为什么foo这个属性会是C?这种应该先从B和B的父类开始找的顺序,我们称之为单调性。
第二种,棱形继承模式,这种模式下面,BFS的查找顺序虽然解了DFS顺序下面的棱形问题,但是它也是违背了查找的单调性。
因为违背了单调性,所以BFS方法只在Python2.2中出现了,在其后版本中用C3算法取代了BFS。
Python2.3到Python2.7:金典类、新式类和平发展
因为之前的BFS存在较大的问题,所以从Python2.3开始新式类的MRO取而代之的是C3算法,我们可以知道C3算法肯定解决了单调性问题,和只能继承无法重写的问题。C3算法具体实现稍后讲解。
MRO的C3算法顺序如下图:看起简直是DFS和BFS的合体有木有。但是仅仅是看起来像而已。
Python3到至今:新式类一统江湖
Python3开始就只存在新式类了,采用的MRO也依旧是C3算法。
【神奇的算法C3】
C3算法解决了单调性问题和只能继承无法重写问题,在很多技术文章包括 官网 中的C3算法,都只有那个merge list的公式法,想看的话网上很多,自己可以查。但是从公式很难理解到解决这个问题的本质。我经过一番思考后,我讲讲我所理解的C3算法的本质。如果错了,希望有人指出来。
假设继承关系如下( 官网 的例子):
class D (object) :
pass
class E (object) :
pass
class F (object) :
pass
class C (D, F) :
pass
class B (E, D) :
pass
class A (B, C) :
pass
if __name__ == '__main__' :
print A.__mro__
首先假设继承关系是一张图(事实上也是),我们按类继承是的顺序(class A(B, C)括号里面的顺序B,C),子类指向父类,构一张图。
我们要解决两个问题:单调性问题和不能重写的问题。
很容易发现要解决单调性,只要保证从根(A)到叶(object),从左到右的访问顺序即可。
那么对于只能继承,不能重写的问题呢?先分析这个问题的本质原因,主要是因为先访问了子类的父类导致的。那么怎么解决只能先访问子类再访问父类的问题呢?如果熟悉图论的人应该能马上想到拓扑排序,这里引用一下百科的的定义:
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
因为拓扑排序肯定是根到叶(也不能说是叶了,因为已经不是树了),所以只要满足从左到右,得到的拓扑排序就是结果,关于拓扑排序算法,大学的数据结构有教,这里不做讲解,不懂的可以自行谷歌或者翻一下书,建议了解完算法再往下看。
那么模拟一下例子的拓扑排序:首先找入度为0的点,只有一个A,把A拿出来,把A相关的边剪掉,再找下一个入度为0的点,有两个点(B,C),取最左原则,拿B,这是排序是AB,然后剪B相关的边,这时候入度为0的点有E和C,取最左。这时候排序为ABE,接着剪E相关的边,这时只有一个点入度为0,那就是C,取C,顺序为ABEC。剪C的边得到两个入度为0的点(DF),取最左D,顺序为ABECD,然后剪D相关的边,那么下一个入度为0的就是F,然后是object。那么最后的排序就为ABECDFobject。
1 2 3 | 对比一下 A .__mro__的结果 (< class ' __main__ . A '>, < class ' __main__ . B '>, < class ' __main__ . E '>, < class ' __main__ . C '>, < class ' __main__ . D '>, < class ' __main__ . F '>, < type ' object '>) |
完全正确!
本应该就这里完了,但是后期一些细心的读者还是发现了问题。以上算法并不完全正确。感谢 @Tiger要好好写论文 指出。
下面我们来看看这个问题:Tiger指出了两点,一点是图中左右顺序比较难区分,还有一点是某种不可序列化的情况下,我的算法会有一些问题,针对这两点我做了改进。
先来看看出错的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class A (object) : pass class B (object) : pass class C (A, B) : pass class D (B, A) : pass class E (C, D) : pass |
构成对应的图,如下其中橙色的线是改进的地方。
如果使用原来的算法,我们搞不清楚A和B谁在左边谁在右边,所以会选择其中之一,继续拓扑下去,其实这里已经是有歧义了不能够解析出正确的顺序,应该报错,这使我重新思考了左右的问题。
我们可以发现其中左右问题无非出现在两种情况,第一种情况是:图中E先继承C,再继承D;第二种情况是:先继承C的基类,再去继承D。针对这两种情况给出的方案就是图中添加的橙色的边,表示的是第一种情况的顺序问题,比如C->D,就是表示E(C,D>中的继承顺序。
那么第二种情况怎么保证先C的基类,然后再考虑D呢。我们可以这么做,如果出现多个入度为0的点,我们先找是刚刚剪出来的点的基类的点。这里可以看之前官网的那个例子,在E点和C点选择的时候,因为E是B的基类点,所以先选它,其实这也很容易实现,只需要记录下每个节点的子类点(可能有多个)。
那么左右的问题也就解决了。