super in python
問題描述
Java 只允許單繼承,創建類很少出現某些奇怪現象,但是 Python 支持多繼承 不熟悉 MRO 有可能導致類無法被創建?不相信請嘗試以下代碼:
O = object
class X(O): pass
class Y(O): pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
具體原因設計到 MRO 所使用的 C3 演算法,筆者有在下面展開分析。
super 在 Python2 與 Python3 之間的區別
以下代碼在 2 和 3 都能夠正常運行
class Child(Base):
def __init__(self):
super(Child, self).__init__()
以下代碼只能在 3 運行
class Child(Base):
def __init__(self):
super().__init__()
super().__init__() 與 Base.__init__(self) 的區別
思考以下以下兩個代碼片段可能產生的效果有什麼區別:
# 1
class Child(Base):
def __init__(self):
super(Child, self).__init__()
# 2
class Child(Base):
def __init(self):
Base.__init__(self)
一定要使用代碼片段 1,而不應該使用代碼片段 2
super(Child, self)可以減少硬編碼為Base
如果 Python 的解析器能夠幫助我們做的事情,我們為什麼一定要硬編碼?如果未來 Child 的父類改變了,忘了改 Base.__init__(self) 那就可能產生災難。Python 是腳本語言並沒有經過完整的編譯,上述錯誤只有在運行時才能夠被發現。
super()可以實現多繼承,造成可能像 C++ 一樣出現基類重複的情況,C++ 的解決方案是虛基類,那麼 Python 呢?
Python 支持多繼承,假設 class Child(Base1, Base2),那麼是不是手動一個一個地調用父類的 __init__ 方法,如以下醜陋且易錯的代碼:
class Child(Base1, Base2):
def __init__(self):
Base1.__init__(self)
Base2.__init__(self)
繼承中一定要使用
super
如果你在生產環境採用了類似的代碼,那麼 code review 的時候很可能被公開批評,特別是在多繼承的結構變得複雜以後,尤其容易出錯。具體分析看下面的 MRO 介紹。
正文
MRO
super() 是根據 MRO(Method Resolution Order) 計算的,而 Python 的 MRO 採用了 C3 演算法。
C3 演算法
有以下的類結構:
O = object
class F(O): pass
class E(O): pass
class D(O): pass
class C(D,F): pass
class B(D,E): pass
class A(B,C): pass
- 設 L[cls] 為類 cls 到其根父類的路徑
- 設 merge(P1, P2, P3) 操作是從 P1...P3 尋找元素 x,其中符合 x 要麼不在 P 中,要麼是 P 的第一個元素,如:
merge(abc, ac, co)
= a + merge(bc, c, co)
= ab + merge(c, c, co)
= abc + merge(o)
= abco
L[O] = O
L[F] = FO
L[E] = EO
L[D] = DO
上述三個我想讀者都不會有異議。下面著重分析 C/B/A:
L[C] = C + merge(L[D], L[F], DF)
= C + merge(DO, FO, DF)
= CD + merge(O, FO, F)
= CDF + merge(O)
= CDFO
L[B] = B + merge(L[D], L[E], DE)
= B + merge(DO, EO, DE)
= BD + merge(O, EO, E)
= BDEO
L[A] = A + merge(L[B], L[C], BC)
= A + merge(BDEO, CDFO, BC)
= AB + merge(DEO, CDFO, C)
= ABC + merge(DEO, DFO)
= ABCD + merge(EO, FO)
= ABCDEFO
所以創建 A 類的 __init__ 和 __new__ 方法調用順序為:A-->B-->C-->D-->E-->F-->O。
讀者可以通過上述代碼,通過 A.mro() 或 A.__mro__ 檢驗是否正確。
分析 C 為什麼無法被創建
回到之前的問題,為什麼 class C 是無法被創建的。
O = object
class X(O): pass
class Y(O): pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
繼承樹的結構如下:
O
/
/
X Y
/ /
/____/____
A B
/
/
/
C
很容易產生錯誤的認識,如果創建類 C 不會產生問題,類似 C++ 中的虛基類初始化順序為:O-->X-->Y-->X-->A-->B-->C,但事實上確實無法創建 class C。
按照上述 C3 演算法計算 L[C]:
L[O] = O
L[X] = XO
L[Y] = YO
L[A] = AXYO
L[B] = BYX0
L[C] = C + merge(L[A], L[B], AB)
= C + merge(AXYO, BYXO, AB)
= CA + merge(XYO, BYXO, B)
= CAB + merge(XYO, YXO) # 無法繼續計算
merge(XYO, YXO) 誤解,因為 X/Y/O 三個元素都不滿足以下兩個條件:
所以 Python 無法確定其初始化的順序,也就無法創建類 C。
__init__ 與 __new__ 的區別
class A:
def __init__(self, *args, **kwargs):
super(A, self).__init__(*args, **kwargs)
def __new__(cls, *args, **kwargs):
return super(A, cls).__new__(cls, *args, **kwargs)
| __init__ | __new__ |
| :---------------------: | :---------------------------: |
| 初始化實例的屬性 | 創建實例 |
| 沒返回值 | 有返回值 |
| 不需要傳遞 self | 需要傳遞 cls |
| 後於 __new__ 調用 | 先於 __init__ 調用 |
| 實例方法,第一個參數是 self | 類方法,第一個參數是 cls |
大多數情況下,我們是不需要重寫父類的 __new__ 方法的,除非需要實現單例模式、不可變數等屬性。元編程可以藉助 __new__ 實現,後面有機會寫一篇關於 Python 元編程的文章。
super().__init__() 並沒有攜帶 self 參數,說明 super() 調用返回的是一個實例。
而為什麼 super().__new__(cls) 需要附帶 cls 參數呢?
那首先得知道 super() 到底返回的是什麼,在不同情況下調用有什麼不同的表現?
我寫了這個小 demo:
from typing import Any
class A:
def __new__(cls) -> Any:
print(A.__new__)
s = super()
return s.__new__(cls)
def __init__(self):
print(A.__init__)
s = super()
s.__init__()
class B(A):
def __new__(cls) -> Any:
print(B.__new__)
s = super()
return s.__new__(cls)
def __init__(self):
print(B.__init__)
s = super()
s.__init__()
class C(B):
def __new__(cls) -> Any:
print(C.__new__)
s = super()
return s.__new__(cls)
def __init__(self):
print(C.__init__)
s = super()
s.__init__()
c = C()
通過打斷點,逐個檢查 super() 的返回值,以及每一個 __new__ 方法中的 cls 參數 和 __init__ 方法中的參數 self 的變化,我得出以下結論:
__new__方法先於__init__方法執行super()似乎每次都返回相同的值__new__返回不是本類的實例,__init__方法也就無法被調用__new__中方法的 cls 一直都是同一個 cls,也就是 cls 一直往下傳遞。我通過id(cls)來判斷的,在 CPython 中,id 方法返回的是內存地址值,我發現id(cls)每次都返回同樣的內容__init__中方法的 self 一直都是同一個 self,驗證方法與__new__一樣
也就是說 super() 是找到 MRO 中下一個父類的 __new__ 和 __init__ 進行調用。
發現了沒,Python 類中如果重寫了某個父類方法 fun(self),但是在某個時刻我們需要調用父類的方法 fun,該如何處理呢?
假設父類為 Base,子類為 Child。除了可以使用 Base.fun(self) 調用外。還可以 super().fun(),當然這種方案只能夠調用在 MRO 中緊跟 Child 的類的方法,但是如果我們想多跳幾級呢?設 Base 的唯一父類為 SuperBase,我們需要在 Child 中調用 SuperBase 的實例方法,我們可以 super(Base, self).fun()。當然在代碼中應該避免這樣的調用,因為下次閱讀代碼需要再次計算 MRO,為了代碼的可讀性,應該這樣調用:SuperBase.fun(self)。
new-style & old-style
在 stackoverflow 看到這樣一個問題:
old-style class 與 new-style class 區別class A: x = a
class B(A): pass
class C(A): x = c
class D(B, C): pass
D.x # a
class A(object): x = a
class B(A): pass
class C(A): x = c
class D(B, C): pass
D.x # c
上述的結果我在 Python3.6 中都無法復現,但是我在 Python2.7 中復現了。因為 "old-style class" 只存在於 Python2,Python3 中只有 new-style class。new-style class 是在 Python2.1 後引入的,以下聲明方法決定其是 new or old style:
# new
class A(object): pass
# old
class A: pass
不使用 super() 實現父類實例初始化
沒有 super,怎麼調用多級的父類 __init__ 方法呢?
還記得我們有 mro() 方法,而且 super() 的初始化順序就是按照 MRO 進行的。
如果沒有 super(),可能需要寫類似以下的代碼:
class Child(Base):
def __init__(self):
mro = type(self).mro()
for next_class in mro[mro.index(Child)+1:]:
if hasattr(next_class, __init__):
# 調用實例方法
next_class.__init__(self)
break
在多繼承中以下代碼還能夠正常運行嗎?
可以。
最後舉個錯誤例子:
class A:
def __init__(self):
print(A.__init__)
super(self.__class__, self).__init__() # 1
class B(A):
def __init__(self):
print(B.__init__)
super(self.__class__, self).__init__() # 2
在 2 處調用 super(self.__class__, self).__init__() 時候傳遞的參數 self 是 B 的實例,所以傳遞到 A.__init__ 1 處 self 依然是 B 的實例,super(self.__class__, self).__init__() 這條語句和 2 產生一樣的效果,繼續執行 A.__init__ 最後導致棧溢出。
參考
Things to Know About Python Super
Python MRO
推薦閱讀:
※【乾貨】初學Python需要安裝哪些軟體?小白必看
※利器系列-更高效的Vim
※用python+opencv優雅地給自己戴個聖誕帽
※leetcode 83. 刪除排序鏈表中的重複元素 python實現
※馬克的Python學習筆記#文件和I/O 3
TAG:Python |
