標籤:

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

  1. 設 L[cls] 為類 cls 到其根父類的路徑
  2. 設 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 三個元素都不滿足以下兩個條件:

1. 要不不存在 P 中

2. 要麼是 P 中的第一個元素

所以 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 |