python中super用法及含義(好文章分享)

python的super用法及含義


註釋:以下都是在python2.7版本驗證的

總括:1、python解決二義性問題,經歷了深度優先算法、廣度優先算法、拓撲排序算法,目前python的版本都是使用拓撲算法(C3)

     2、嚴謹super(A,self).__init__()和A.__init__(self)這兩種調用類方法的方法混用

         3、A.__init__(self)是經典類的調用方法,使用深度優先算法,不論是否有類繼承object;也就是新式類也可以使用這種調用方法

         4、super(A,self).__init__()是新式類的調用方法,使用C3算,及拓撲算法;super也是一個類,不是一個方法,必須用到新式類上

     5. super並不是一個函數,是一個類名,形如super(B, self)事實上調用了super類的初始化函數,產生了一個super對象;
     6. super類的初始化函數並沒有做什麼特殊的操作,只是簡單記錄了類類型和具體實例;
     7. super(B, self).func的調用並不是用於調用當前類的父類的func函數;使用C3算法,先搜索到func函數纔是,父類的func函數不見得就一定先搜索到;super(self,C).func()    #調用的並不是其父類C的func,而是C在MRO中的下一個類的func,不能再經典類中使用
     8. Python的多繼承類是通過mro的方式來保證各個父類的函數被逐一調用,而且保證每個父類函數只調用一次(如果每個類都使用super);
     9. 混用super類和非綁定的函數是一個危險行爲,這可能導致應該調用的父類函數沒有調用或者一個父類函數被調用多次。

一、相關概念:

MRO:Method Resolution Order,即方法解析順序,是python中用於處理二義性問題的算法

經典類:  反之,即不由任意內置類型派生出的類,則稱之爲“經典類”,mro使用“深度優先算法”

新式類:在Python 2及以前的版本中,由任意內置類型派生出的類(只要一個內置類型位於類樹的某個位置),都屬於“新式類”,都會獲得所有“新式類”的特性;mro使用“拓撲算法,也成C3算法”;新式類,可以通過調用mro()函數和__mro__屬性查看類的繼承關係(方法:類名.mro(); 類名.__mro__)

python3.x: “新式類”和“經典類”的區分在Python 3之後就已經不存在,在Python 3.x之後的版本,因爲所有的類都派生自內置類型object(即使沒有顯示的繼承object類型),即所有的類都是“新式類”。

定義方式不同:

在Python 2.x 版本中,默認類都是舊式類,除非顯式繼承object。在Python 3.x 版本中,默認類就是新式類,無需顯示繼承object。

在Python 2.x 中,定義舊式類的方式:

class A:  # A是舊式類,因爲沒有顯示繼承object
    pass

class B(A):  # B是舊式類,因爲B的基類A是舊式類
    pass
定義新式類的方式:

class A(object):  # A是新式類,因爲顯式繼承object
    pass

class B(A):  # B是新式類,因爲B的基類A是新式類
    pass
2. 保持class與type的統一

對新式類的實例執行a.__class__與type(a)的結果是一致的,對於舊式類來說就不一樣了。

複製代碼
class A():
   pass
class B(object):
   pass   
a=A() 
b=B() 
print type(a)
print a.__class__
print type(B)
print B.__class__ 
print type(b)
print b.__class__

輸出:
<type 'instance'>
__main__.A
<type 'type'>
<type 'type'>
<class '__main__.B'>
<class '__main__.B'>
[Finished in 1.5s]
複製代碼

二、二義性:

python支持多繼承,多繼承的語言往往會遇到以下兩類二義性的問題:

有兩個基類A和B,A和B都定義了方法f(),C繼承A和B,那麼調用C的f()方法時會出現不確定。
有一個基類A,定義了方法f(),B類和C類繼承了A類(的f()方法),D類繼承了B和C類,那麼出現一個問題,D不知道應該繼承B的f()方法還是C的f()方法。
python中通過C3算法很好的避免了以上兩類二義性的情況。

深度優先算法(DFS,Depth-First-Search)
把根節點壓入棧中。
每次從棧中彈出一個元素,搜索所有在它下一級的元素,把這些元素壓入棧中。並把這個元素記爲它下一級元素的前驅。
找到所要找的元素時結束程序。
如果遍歷整個樹還沒有找到,結束程序。
廣度優先算法(BFS,Breadth-First-Search)
把根節點放到隊列的末尾。
每次從隊列的頭部取出一個元素,查看這個元素所有的下一級元素,把它們放到隊列的末尾。並把這個元素記爲它下一級元素的前驅。
找到所要找的元素時結束程序。
如果遍歷整個樹還沒有找到,結束程序。
拓撲排序:
  對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊(u,v)∈E(G),則u在線性序列中出現在v之前。通常,這樣的線性序列稱爲滿足拓撲排序(TopologicalOrder)的序列,簡稱拓撲序列。

拓撲排序的實現步驟:

循環執行以下兩步,直到不存在入度爲0的頂點爲止

選擇一個入度爲0的頂點並輸出之;
從網中刪除此頂點及所有出邊。
python中調用父類方法的兩種方式:

複製代碼
class A(object):
   def __init__(self):
       self.name = "A: name"
       print "A:__init__"
   def fun(self):
       print "A:fun"

class B(A):
   def __init__(self):
       print "B:__init__"
       A.__init__(self)                               # 使用類名直接調用
       super(B, self).__init__()                # 使用super關鍵字調用
   def fun(self):
       print "B:fun"
       A.fun(self)
       super(B, self).fun()
       print self.name
複製代碼
對於單繼承來說,上面這兩種方式並無本質上的區別,但是當出現多繼承的時候,super得到的基類就不一定是我們“認爲”的基類了,我們看下面這個例子:

複製代碼
class A(object):
   def __init__(self):
       print "enter A"
       print "leave A"

class B(object):
   def __init__(self):
       print "enter B"
       print "leave B"

class C(A):
   def __init__(self):
       print "enter C"
       super(C, self).__init__()
       print "leave C"

class D(A):
   def __init__(self):
       print "enter D"
       super(D, self).__init__()
       print "leave D"

class E(B, C):
   def __init__(self):
       print "enter E"
       B.__init__(self)
       C.__init__(self)
       print "leave E"

class F(E, D):
   def __init__(self):
       print "enter F"
       E.__init__(self)
       D.__init__(self)
       print "leave F"

f = F()
複製代碼
輸出結果:

enter F

enter E

enter B

leave B

enter C

enter D

enter A

leave A

leave D

leave C

leave E

enter D

enter A

leave A

leave D

leave F

類的繼承關係如下所示:

   object

  |       \

  |        A

  |      / |

  B       C  D

   \   /   |

     E     |

       \   |

         F

我們的本意是希望調用構造函數的時候,對於基類的構造方法也進行調用,但是實際結果發現,A和D的構造函數被調用了2次,而且奇怪的是,當調用super(C, self).__init__()的時候,竟然進入D的構造函數,這也是爲什麼D的構造函數被調用了兩次(一次是F調用的,一次是C調用的)!從繼承關係上看,C的基類應該是A纔對。這就要引出下面要解釋的,python中的C3方法。不過針對上面這個例子,修改的思路很簡單,要麼全部使用類名來調用基類方法,要麼全部使用super()來調用,不要混用!

三、C3算法的演變歷史:

經典類(python 2.2之前):

  在python 2.2之前,python中使用經典類(classicclass),經典類是一種沒有繼承的類,所有類型都是type類型,如果經典類作爲父類,子類調用父類構造函數會報錯。當時用作MRO的算法是DFS(深度優先),下面的例子是當時使用DFS算法的示例(向右是基類方向):

正常的繼承方式:
A->B->D

A->C->E

DFS的遍歷順序爲:A->B->D->C->E

這種情況下,不會產生問題。

菱形的繼承方式
A->B->D

A->C->D

DFS的遍歷順序爲:A->B->D->C

對於這種情況,如果公共父類D中也定義了f(),C中重寫了方法f(),那麼C中的f()方法永遠也訪問不到,因爲按照遍歷的順序始終先發現D中的f()方法,導致子類無法重寫基類方法。

新式類(python2.2):

在python2.2開始,爲了使類的內置類型更加統一,引入了新式類(new-style class),新式類每個類都繼承自一個基類,默認繼承自object,子類可以調用基類的構造函數。由於所有類都有一個公共的祖先類object,所以新式類不能使用DFS作爲MRO。在當時有兩種MRO並存:

如果是經典類,MRO使用DFS

如果是新式類,MRO使用BFS

針對新式類的BFS示例如下(向右是基類方向):

正常繼承方式:
A->B->D

A->C->E

BFS的遍歷順序爲:A->B->C->D->E

D是B的唯一基類,但是遍歷時卻先遍歷節點C,這種情況下應該先從唯一基類進行查找,這個原則稱爲單調性。

菱形的繼承方式
A->B->D

A->C->D

BFS的遍歷順序爲:A->B->C->D

BFS解決了前面提到的子類無法重寫基類方法的問題。

經典類和新式類並存(python2.3-python2.7),C3算法產生:

由於DFS和BFS針對經典類和新式類都有缺陷,從python2.3開始,引入了C3算法。針對前面兩個例子,C3算法的遍歷順序如下:

正常繼承方式:
A->B->D

A->C->E

C3的遍歷順序爲:A->B->D->C->E

菱形的繼承方式
A->B->D

A->C->D

C3的遍歷順序爲:A->B->C->D

看起來是DFS和BFS的綜合,但是並非如此,下面的例子說明了C3算法的具體實現:

從前面拓撲排序的定義可知,將有向無環圖進行拓撲排序後,按照得到的拓撲序列遍歷即可滿足單調性,原因是由根到葉即是子類到基類的方向,當基類的入度爲0是,它就是子類的唯一基類,此時會優先遍歷此基類,符合單調性。而子類無法重寫方法的問題也可以得到解決,因爲當多個子類繼承自同一個基類時,該基類的入度不會先於子類減爲0,所以可以保證優先遍歷入度減爲0的子類。

結合下面這張圖的例子來說明C3算法的執行步驟(圖中箭頭由子類指向父類):

首先找入度爲0的點,只有A,把A取出,把A相關的邊去掉,再找下一個入度爲0的點,B和C滿足條件,從左側開始取,取出B,這時順序是AB,然後去掉B相關的邊,這時候入度爲0的點有E和C,依然取左邊的E,這時候順序爲ABE,接着去掉E相關的邊,這時只有一個點入度爲0,那就是C,取C,順序爲ABEC。去掉C的邊得到兩個入度爲0的點D和F,取出D,順序爲ABECD,然後去掉D相關的邊,那麼下一個入度爲0的就是F,然後是object。所以最後的排序就爲ABECDFobject。

瞭解了C3算法,我們前面那個混用的例子中調用super(C,self).__init__()會去調用D構造函數的原因也就顯而易見了。

在python中提供了__mro__內置屬性來查看類的MRO,例如:

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

print A.__mro__
print A.mro()

#輸出:(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <type 'object'>)

再舉一個例子:

class A(object):  
    def __init__(self):  
        print "A"  

class B(object):  
    def __init__(self):  
        print "B"  

class C(object):  
    def __init__(self):  
        print "C"  

class D(A,B,C):  
    def __init__(self):  
        super(D,self).__init__()  
        super(A,self).__init__()  
        super(B,self).__init__()  
        super(C,self).__init__()  

X  = D()  
print D.mro()
# D-->A-->B-->C-->object D的直接父類是A,A的直接父類是B,。。。。

輸出是:

A
B
C

[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <type 'object'>]
[Finished in 1.2s]

會發現:

super(D,self).__init__()  
執行的是A.__init__()

super(A,self).__init__()  
執行的是B.__init__()

super(B,self).__init__()  
執行的是C.__init__()

super(C,self).__init__()  
執行的是Object.__init__()

這是因爲mro(D)爲:[ D, A, B, C, Object]

mro的C3算法,參考這篇文章:https://www.cnblogs.com/whatisfantasy/p/6046991.html

我們把類 C 的線性化(MRO)記爲 L[C] = [C1, C2,…,CN]。其中 C1 稱爲 L[C] 的頭,其餘元素 [C2,…,CN] 稱爲尾。如果一個類 C 繼承自基類 B1、B2、……、BN,那麼我們可以根據以下兩步計算出 L[C]:

L[object] = [object]
L[C(B1…BN)] = [C] + merge(L[B1]…L[BN], [B1]…[BN])
這裏的關鍵在於 merge,其輸入是一組列表,按照如下方式輸出一個列表:

檢查第一個列表的頭元素(如 L[B1] 的頭),記作 H。
若 H 未出現在其它列表的尾部(又是頭又是尾的除外),則將其輸出,並將其從所有列表中刪除,然後回到步驟1;否則,取出下一個列表的頭部記作 H,繼續該步驟。
重複上述步驟,直至列表爲空或者不能再找出可以輸出的元素。如果是前一種情況,則算法結束;如果是後一種情況,說明無法構建繼承關係,Python 會拋出異常。

根據圖示,查看各個類的線性化結果

L[object] = [object]

L[X] = L[X(object)] = [X]+merge(L[object]+[object]) = [X] + merge([object] + [object]) = [X] + [object] =  [X, object]

L[Y] = [Y, object]

L[A] = L[A(X,Y)] = [A] + merge(L[X], L[Y], [X], [Y]) = [A] + merge([X, object], [Y, object], [X], [Y]) = [A, X] + merge([object], [Y, object], [Y]) = [A, X, Y] + merge([object], [object]) = [A, X, Y, object]

轉自https://www.cnblogs.com/shengulong/p/7892266.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章