天天看點

Python面試題之Super函數

這是個高大上的函數,在python裝13手冊裡面介紹過多使用可顯得自己是高手 23333. 但其實他還是很重要的. 簡單說, ​

​super​

​函數是調用下一個父類(超類)并傳回該父類執行個體的方法. 這裡的下一個的概念參考後面的MRO表介紹.

help介紹如下:

super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type) -> unbound super object
super(type, type2) -> bound super object; requires issubclass(type2, type)
Typical use to call a cooperative superclass method:
class C(B):
    def meth(self, arg):
        super(C, self).meth(arg)      

由此可知, super有三種用法, 第一參數總是召喚父類的那個類, 第二參數可缺(傳回非綁定父類對象),也可以是執行個體對象或該類的子類. 最終傳回的都是父類的執行個體(綁定或非綁定). 在Python3中,super函數多了一種用法是直接​

​super()​

​,相當于​

​super(type,首參)​

​, 這個首參就是一般的傳入的​

​self​

​執行個體本身啦. 因為在py2裡面常用也是這種寫法.

另外, 在py2中, super隻支援新類( new-style class, 就是繼承自object的).

為什麼要調用父類?

在類繼承時, 要是重定義某個方法, 這個方法就會覆寫掉父類的相應同名方法. 通過調用父類執行個體, 可以在子類中同時實作父類的功能.例如:

# Should be new-class based on object in python2.
class A(object):
    def __init__(self):
        print "enter A"
        print "leave A"

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

>>> b = B()
enter B
enter A
leave A
leave B      

通過調用​

​super​

​獲得父類執行個體進而可以實作該執行個體的初始化函數. 這在實踐中太常用了 (因為要繼承父類的功能, 又要有新的功能).

直接使用父類來調用的差異

事實上, 上面的super函數方法還可以這麼寫:

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

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

通過直接使用父類類名來調用父類的方法, 實際也是可行的. 起碼在上面的例子中效果上他們現在是一樣的. 這種方法在老式類中也是唯一的調用父類的方法 (老式類沒有super).

通過父類類名調用方法很常用, 比較直覺. 但其效果和super還是有差異的. 例如:

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

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

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

class D(B,C):
    def __init__(self):
        print "enter D"
        B.__init__(self)
        C.__init__(self)
        print "leave D"
>>> d=D()
enter D
enter B
enter A
leave A
leave B
enter C
enter A
leave A
leave C
leave D      

可以發現, 這裡面A的初始化函數被執行了兩次. 因為我們同時要實作B和C的初始化函數, 是以分開調用兩次, 這是必然的結果.

但如果改寫成super呢?

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

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

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

class D(B,C):
    def __init__(self):
        print "enter D"
        super(D,self).__init__()
        print "leave D"
>>> d=D()
enter D
enter B
enter C
enter A
leave A
leave C
leave B
leave D      

會發現所有父類ABC隻執行了一次, 并不像之前那樣執行了兩次A的初始化.

然後, 又發現一個很奇怪的: 父類的執行是 BCA 的順序并且是全進入後再統一出去. 這是MRO表問題, 後面繼續讨論.

如果沒有多繼承, ​

​super​

​其實和通過父類來調用方法差不多. 但, super還有個好處: 當B繼承自A, 寫成了​

​A.__init__​

​, 如果根據需要進行重構全部要改成繼承自 ​

​E​

​,那麼全部都得改一次! 這樣很麻煩而且容易出錯! 而使用​

​super()​

​就不用一個一個改了(隻需類定義中改一改就好了)Anyway, 可以發現, ​

​super​

​并不是那麼簡單.

MRO 表

MRO是什麼? 可以通過以下方式調出來:

>>> D.mro() # or d.__class__.mro()  or D.__class__.mro(D) 
[D, B, C, A, object]

>>> B.mro()
[B, A, object]

>>> help(D.mro)
#Docstring:
#mro() -> list
#return a type's method resolution order
#Type:      method_descriptor      

MRO就是類的方法解析順序表, 其實也就是繼承父類方法時的順序表 (類繼承順序表去了解也行) 啦.

這個表有啥用? 首先了解實際super做了啥:

def super(cls, inst):
    mro = inst.__class__.mro()
    return mro[mro.index(cls) + 1]      

換而言之, super方法實際是調用了​

​cls​

​的在MRO表中的下一個類. 如果是簡單一條線的單繼承, 那就是父類->父類一個一個地下去羅. 但對于多繼承, 就要遵循MRO表中的順序了. 以上面的D的調用為例:

d的初始化
-> D (進入D) super(D,self) 
-> 父類B (進入B) super(B,self) 
-> 父類C (進入C) super(C,self) 
-> 父父類A (進入A)  (退出A) # 如有繼續super(A,self)  -> object (停了)
-> (退出C)
-> (退出B)
-> (退出D)      

是以, 在MRO表中的超類初始化函數隻執行了一次!

那麼, MRO的順序究竟是怎麼定的呢? 這個可以參考官方說明​​The Python 2.3 Method Resolution Order​​. 基本就是, 計算出每個類(從父類到子類的順序)的MRO, 再merge 成一條線. 遵循以下規則:

在 MRO 中,基類永遠出現在派生類後面,如果有多個基類,基類的相對順序保持不變。 這個原則包括兩點:

  1. 基類永遠在派生類後面
  2. 類定義時的繼承順序影響相對順序.
Python面試題之Super函數

那麼MRO是: ​

​F -> E -> B -> C -> D -> A -> object​

怎麼解釋呢?

根據官方的方法, 是:

L(O) = O
L(B) = B O
L(A) = A O
L(C) = C A O
L(D) = D A O
L(E) = E + merge(L(B),L(C))
     = E + merge(BO,CAO)
     = E + B + merge(O,CAO)
     = E + B + C + merge(O,AO)
     = E + B + C + A + merge(O,O)
     = E B C A O
L(F) = F + merge(L(E),L(D))
     = F + merge(EBCAO,DAO)
     = F + EBC + merge(AO,DAO)
     = F + EBC + D + merge(AO,AO)
     = F EBC D AO      

看起來很複雜..但還是遵循在 MRO 中,基類永遠出現在派生類後面,如果有多個基類,基類的相對順序保持不變。是以, 我個人認為可以這麼想:

  • 先找出最長深度最深的繼承路線F->E->C->A->object

    . (因為必然基類永遠出現在派生類後面)

  • 類似深度優先, 定出其餘順序:F->E->B->obj

    ,F->D->A-object

  • 如果有多個基類,基類的相對順序保持不變, 類似于merge時優先提前面的項. 是以排好這些路線: (FEBO, FECAO, FDAO)
  • F->E->B->obj

    且E(B,C)決定B在C前面.是以F->E->B->C->A->obj

    (相當于F+merge(EBO,ECAO)

    ).

  • F->D->A-object

    且F(E,D)決定了D在E後, 是以D在E後A前. 因為相對順序, 相當于FE+merge(BCAO, DAO)

    , 是以FE BC D AO

 super 是個類

 當我們調用 super() 的時候,實際上是執行個體化了一個 super 類。你沒看錯, super 是個類,既不是關鍵字也不是函數等其他資料結構:

>>> class A: pass
...
>>> s = super(A)
>>> type(s)
<class 'super'>
>>>      

在大多數情況下, super 包含了兩個非常重要的資訊: 一個 MRO 以及 MRO 中的一個類。當以如下方式調用 super 時:

super(a_type, obj)      

MRO 指的是 type(obj) 的 MRO, MRO 中的那個類就是 a_type , 同時 isinstance(obj, a_type) == True 。

當這樣調用時:

super(type1, type2)      

MRO 指的是 type2 的 MRO, MRO 中的那個類就是 type1 ,同時 issubclass(type2, type1) == True 。

那麼, super() 實際上做了啥呢?簡單來說就是:提供一個 MRO 以及一個 MRO 中的類 C , super() 将傳回一個從 MRO 中 C 之後的類中查找方法的對象。

也就是說,查找方式時不是像正常方法一樣從所有的 MRO 類中查找,而是從 MRO 的 tail 中查找。

舉個栗子, 有個 MRO:

[A, B, C, D, E, object]      

下面的調用:

super(C, A).foo()      

super 隻會從 C 之後查找,即: 隻會在 D 或 E 或 object 中查找 foo 方法。

多繼承中 super 的工作方式

再回到前面的

d = D()
d.add(2)
print(d.n)      

現在你可能已經有點眉目,為什麼輸出會是

self is <__main__.D object at 0x10ce10e48> @D.add
self is <__main__.D object at 0x10ce10e48> @B.add
self is <__main__.D object at 0x10ce10e48> @C.add
self is <__main__.D object at 0x10ce10e48> @A.add
19      

下面我們來具體分析一下:

  • D 的 MRO 是: [D, B, C, A, object] 。備注: 可以通過 D.mro() (Python 2 使用 D.__mro__ ) 來檢視 D 的 MRO 資訊)
  • 詳細的代碼分析如下:
class A:
    def __init__(self):
        self.n = 2

    def add(self, m):
        # 第四步
        # 來自 D.add 中的 super
        # self == d, self.n == d.n == 5
        print('self is {0} @A.add'.format(self))
        self.n += m
        # d.n == 7


class B(A):
    def __init__(self):
        self.n = 3

    def add(self, m):
        # 第二步
        # 來自 D.add 中的 super
        # self == d, self.n == d.n == 5
        print('self is {0} @B.add'.format(self))
        # 等價于 suepr(B, self).add(m)
        # self 的 MRO 是 [D, B, C, A, object]
        # 從 B 之後的 [C, A, object] 中查找 add 方法
        super().add(m)

        # 第六步
        # d.n = 11
        self.n += 3
        # d.n = 14

class C(A):
    def __init__(self):
        self.n = 4

    def add(self, m):
        # 第三步
        # 來自 B.add 中的 super
        # self == d, self.n == d.n == 5
        print('self is {0} @C.add'.format(self))
        # 等價于 suepr(C, self).add(m)
        # self 的 MRO 是 [D, B, C, A, object]
        # 從 C 之後的 [A, object] 中查找 add 方法
        super().add(m)

        # 第五步
        # d.n = 7
        self.n += 4
        # d.n = 11


class D(B, C):
    def __init__(self):
        self.n = 5

    def add(self, m):
        # 第一步
        print('self is {0} @D.add'.format(self))
        # 等價于 super(D, self).add(m)
        # self 的 MRO 是 [D, B, C, A, object]
        # 從 D 之後的 [B, C, A, object] 中查找 add 方法
        super().add(m)

        # 第七步
        # d.n = 14
        self.n += 5
        # self.n = 19

d = D()
d.add(2)
print(d.n)      

調用過程圖如下:

D.mro() == [D, B, C, A, object]
d = D()
d.n == 5
d.add(2)

class D(B, C):          class B(A):            class C(A):             class A:
    def add(self, m):       def add(self, m):      def add(self, m):       def add(self, m):
        super().add(m)  1.--->  super().add(m) 2.--->  super().add(m)  3.--->  self.n += m
        self.n += 5   <------6. self.n += 3    <----5. self.n += 4     <----4. <--|
        (14+5=19)               (11+3=14)              (7+4=11)                (5+2=7)      
Python面試題之Super函數

現在你知道為什麼 d.add(2) 後 d.n 的值是 19 了吧 ;)

參考

​​參考1​​

​​參考2​​