详解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顺序如下图:




python多类继承 python多继承 super_Python




两种继承模式在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顺序如下图:

python多类继承 python多继承 super_python_02





两种继承模式在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的合体有木有。但是仅仅是看起来像而已。

python多类继承 python多继承 super_python_03






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),子类指向父类,构一张图。

python多类继承 python多继承 super_python多类继承_04



我们要解决两个问题:单调性问题和不能重写的问题。

很容易发现要解决单调性,只要保证从根(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



构成对应的图,如下其中橙色的线是改进的地方。

python多类继承 python多继承 super_python多类继承_05



如果使用原来的算法,我们搞不清楚A和B谁在左边谁在右边,所以会选择其中之一,继续拓扑下去,其实这里已经是有歧义了不能够解析出正确的顺序,应该报错,这使我重新思考了左右的问题。

我们可以发现其中左右问题无非出现在两种情况,第一种情况是:图中E先继承C,再继承D;第二种情况是:先继承C的基类,再去继承D。针对这两种情况给出的方案就是图中添加的橙色的边,表示的是第一种情况的顺序问题,比如C->D,就是表示E(C,D>中的继承顺序。

那么第二种情况怎么保证先C的基类,然后再考虑D呢。我们可以这么做,如果出现多个入度为0的点,我们先找是刚刚剪出来的点的基类的点。这里可以看之前官网的那个例子,在E点和C点选择的时候,因为E是B的基类点,所以先选它,其实这也很容易实现,只需要记录下每个节点的子类点(可能有多个)。

那么左右的问题也就解决了。