Python類繼承的高級特性

昨天在Python類的多重繼承那裏糾結了很久,在提問版塊提了個問題探討了探討(連接)才徹底搞明白,如今把類的特性整理下,供之後參考

正文

首先得說明的是,Python的類分爲經典類新式類
經典類是python2.2以前的東西,可是在2.7還在兼容,可是在3以後的版本就只認可新式類了
新式類在python2.2以後的版本中均可以使用html

經典類和新式類的區別在於:

  1. 經典類是默認沒有派生自某個基類的,而新式類是默認派生自object這個基類的:
# old style
class A():pass

# new style
class A(obejct):pass

2.經典類在類多重繼承的時候是採用從左到右深度優先原則匹配方法的..而新式類是採用C3算法(不一樣於廣度優先)進行匹配的python

3.經典類是沒有__MRO__instance.mro()調用的,而新式類是有的.算法

爲何不用經典類,要更換到新式類

由於在經典類中的多重繼承會有些問題...可能致使在繼承樹中的方法查詢繞事後面的父類:編程

class A():
    def foo1(self):
        print "A"
class B(A):
    def foo2(self):
        pass
class C(A):
    def foo1(self):
        print "C"
class D(B, C):
    pass

d = D()
d.foo1()

按照經典類的查找順序從左到右深度優先的規則,在訪問d.foo1()的時候,D這個類是沒有的..那麼往上查找,先找到B,裏面沒有,深度優先,訪問A,找到了foo1(),因此這時候調用的是A的foo1(),從而致使C重寫的foo1()被繞過.segmentfault

因此python引入了新式類的概念,每一個基類都繼承自object而且,他的匹配規則也從深度優先換到了C3函數


C3算法

C3算法是怎麼作匹配的呢..在問答版塊上面討論以後,歸結以下:code

C3算法的一個核心是merge.htm

在merge列表中,若是第一個序列mro的第一個類是出如今其它序列,而且也是第一個,或者不出現其它序列,那麼這個類就會從這些序列中刪除,併合到訪問順序列表中blog

好比:(引用問題中zhuangzebo的回答@zhuangzebo)繼承

class A(O):pass
class B(O):pass
class C(O):pass
class D(A,B):pass
class E(C,D):pass

首先須要知道 O(object)的mro(method resolution order)列表是[O,]
那麼接下來是:

mro(A) = [A, O]
mro(B) = [B, O]
mro(C) = [C, O]
mro(D) = [D] + merge(mro(A), mro(B), [A, B])
= [D] + merge([A, O], [B, O], [A, B])
= [D, A] + merge([O], [B, O], [B])
= [D, A, B] + merge([O], [O])
= [D, A, B, O]
mro(E) = [E] + merge(mro(C), mro(D), [C, D])
= [E] + merge([C, O], [D, A, B, O], [C, D])
= [E, C] + merge([O], [D, A, B, O], [D])
= [E, C, D] + merge([O], [A, B, O])
= [E, C, D, A, B] + merge([O], [O])
= [E, C, D, A, B, O]

而後還有一種特殊狀況:
好比:
merge(DO,CO,C) 先merge的是D
merge(DO,CO,C) 先merge的是C
意思就是.當出現有 一個類出如今兩個序列的頭(好比C) 這種狀況和 這個類只有在一個序列的頭(好比D) 這種狀況同時出現的時候,按照順序方式匹配。

新式類生成的訪問序列被存儲在一個叫MRO的只讀列表中..
你可使用instance.__MRO__或者instance.mro()來訪問

最後匹配的時候就按照MRO序列的順序去匹配

C3和廣度優先的區別:

舉個例子就徹底明白了:

class A(object):pass
class B(A):pass
class C(B):pass
class D(A):pass
class E(D):pass
class F(C, E):pass

按照廣度優先遍歷,F的MRO序列應該是[F,C,E,B,D,A]
可是C3是[F,E,D,C,B,A]
意思是你能夠當作C3是在一條鏈路上深度遍歷到和另一條鏈路的交叉點,而後去深度遍歷另一條鏈路,最後遍歷交叉點

新式類和經典類的super按類名訪問問題

在經典類中,你若是要訪問父類的話,是用類名來訪問的..

class A():
    def __init__(self):
        print "A"
class B(A):
    def __init__(self):
        print "B"
        A.__init__(self)  #python不會默認調用父類的初始化函數的

這樣子看起來沒三問題,可是若是類的繼承結構比較複雜,會致使代碼的可維護性不好..
因此新式類推出了super這個東西...

class A():
    def __init__(self):
        print "A"
class B(A):
    def __init__(self):
        print "B"
        super(B,self).__init__()

這時候,又有一個問題:當類是多重繼承的時候,super訪問的是哪個類呢?
super其實是經過__MRO__序列來肯定訪問哪個類的...實際上就是調用__MRO__中此類後面的一個類的方法.
好比序列爲[F,E,D,C,B,A]那麼F中的super就是E,E的就是D

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"

這時候打印出來是:

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

能夠看出來D和A的初始化函數被亂入了兩次!
按類名訪問就至關於C語言以前的GOTO語句...亂跳,而後再用super按順序訪問..就有問題了

因此建議就是要麼一直用super,要麼一直用按照類名訪問

最佳實現:

  1. 避免多重繼承
  2. super使用一致
  3. 不要混用經典類和新式類
  4. 調用父類的時候注意檢查類層次

參考資料:

  1. 《python高級編程》
  2. http://www.cnblogs.com/lovemo1314/archive/2011/05/03/2035005.html
  3. http://www.cnblogs.com/i2u9/archive/2013/03/19/pythonmroc3.html
相關文章
相關標籤/搜索