相信寫 OOP 的人對於繼承這個概念應該不陌生,Python 身為一個支援 OOP 的語言,自然也支援繼承。由於 Python 2 已經慢慢被淘汰,下面的例子會以 Python 3 為準做說明。
Python 繼承極簡介
class Equus:
def __init__(self, is_male):
pass def run(self):
print(f'I run at speed {self.speed}')
print(f'I\'m {self.gender}')class Horse(Equus):
def __init__(self, is_male):
print('Horse init')
self.speed = 30
self.gender = 'male' if is_male else 'female'
self.is_horse = True def roar(self):
print('Hee haw~')class Donkey(Equus):
def __init__(self, is_female):
print('Donkey init')
self.speed = 20
self.gender = 'female' if is_female else 'male'
self.is_donkey = True def roar(self):
print('Hee haw hee hee haw~')
這邊我們有兩個 class
,馬 (Horse
)與驢子 (Donkey
),都是馬屬 (Equus
),馬屬的東 (ㄨˋ) 西 (ㄐㄧㄢˋ) 都會跑 (run
)。
這邊我們可以看到我們利用繼承達到程式碼的復用,馬跟驢子共用了馬屬的 run
method,而馬跟驢有不同的叫聲 (roar
)。
到目前為止,相信身為 Python 工程師,應該都不陌生。接著,讓我們聊聊 super
吧。
super
的美麗與哀愁
super 美麗
假設我們現在想寫個 MiniHorse
繼承 Horse
,並覆寫 roar
方法,但同時又想使用到 Horse
的 roar
,你可以這樣寫:
class MiniHorse(Horse):
def __init__(self, is_male, leg_length):
super().__init__(is_male)
self.leg_length = leg_length def roar(self):
super().roar()
print('mini mini mini')
這邊是 super
的基本用法,分別在 __init__
裡用 super
去使用 Horse
。或許會想,所以 super
就是 Horse
? 這邊比較一下不用 super
的寫法:
class MiniHorse(Horse):
def __init__(self, is_male):
Horse.__init__(self, is_male)
self.leg_length = leg_length
def roar(self):
Horse.roar(self)
print('mini mini mini')
前者是所謂的 bound method 的呼叫方式,後者是用 unbound method 的呼叫方式,在這個例子裡兩者等價。(有興趣的讀者再自行 google 吧,建議關鍵字有 unbound、bound method
與 static method
)
看看 super
的說明:
所以說, super
是一種 type
,用於建立 super
物件。在第一種寫法裡,我們用 super()
建立一個 bounded 的 super object ,因此可以使用 Horse
的 物件方法。這也是 super
最常見的用法,用於呼叫 super class 的方法。
可是也可以不用 super
做到一樣的事,那到底要它幹嘛?
多重繼承 (multiple inheritance)
要 super
幹嘛? 想回答這個問題,就不得不聊聊 Python 的多重繼承。這可是連 Java 都做不到的事呢! (Java 必須使用 interface)
不過這邊先讓我賣個關子,讓我用 roar
來體現 super
的用途。之後會回頭講講 __init__
,會有趣很多。
讓我們稍稍改寫一下上面的範例
class Equus:
def __init__(self, is_male):
pass def run(self):
print(f'I run at speed {self.speed}')
print(f'I\'m {self.gender}')
def roar(self):
print('Equus roars')class Horse(Equus):
def __init__(self, is_male):
print('Horse init')
self.speed = 30
self.gender = 'male' if is_male else 'female'
self.is_horse = True def roar(self):
print('Horse: Hee haw~')
super().roar()class Donkey(Equus):
def __init__(self, is_female):
print('Donkey init')
self.speed = 20
self.gender = 'female' if is_female else 'male'
self.is_donkey = True def roar(self):
print('Donkey: Hee haw hee hee haw~')
super().roar()
有發現我改了什麼嗎? ( = w = )b
接著,我們使用多重繼承實作騾子(Mule
):
class Mule(Horse, Donkey):
def __init__(self, is_male):
print('Mule init')
super().__init__(is_male)
def roar(self):
print('Mule: Muuuuleee~~~')
super().roar()
騾子是馬跟驢的混種,我們希望牠的行為可以混雜著兩者個特性。
執行的結果:
很好,這個騾子叫的方式果然如我們所希望的,同時混雜著馬跟驢的叫聲。
這個結果簡單的流程可以想成這樣:
Mule.roar
被呼叫,print('Mule: ...')
被執行Mule.roar
中的super().roar()
被執行,Horse.roar
被呼叫,print('Horse: ...')
被執行- 同理,
Donkey.roar
也被執行 (因為Horse.roar
裡的super
- 最後
Equus
的roar
被呼叫
看到 3 你或許會覺得奇怪,為什麼在 Donkey
的 roar
裡的 super
在這個時候變成 Horse
了? Donkey
的 super class 不是應該是 Equus
嗎?
MRO and C3 Linearization
嚴格來說,說 super()
是用來使用 super class 的方法並不是一個正確的說法。比較正確的說法應該是它會建立一個使用 mro 中下一個 type 的 bounded proxy object。(WTF...)
沒關係,一個一個來。首先是 mro。
呼叫 Mule.mro
,你應該會看到:
Mule.mro
的說明:
簡單的說,mro 是在多重繼承的時候,決定該使用哪一個 parent class 的 method 的搜尋路徑。而這個路徑,是由 C3 Linearization 這個演算法所決定。
隨便 google 一下就會有一堆 C3 Linearization 的說明,這邊就不贅述了。
以這邊的 Mule
來說,因為是宣告成 class Mule(Horse, Donkey): ...
,所以 mro 中, Mule
之後先是 Horse
,再來是 Donkey
。
所以整個 Mule.roar
被呼叫時的 super
被 bound 成 Horse
,所以這時候的 super().roar
會是 Horse
的 instance method call ;然後在 Horse.roar
裡 super
再次被呼叫,建立了 Donkey
的 bound proxy object ,所以這時的 super().roar
會變成 Donkey.roar
的 instance method call ;最後, Donkey.roar
裡的 super
就被 proxy 到 Equus
去了。這就是 Python 使用 super
實現多重繼承的方式。
所以說,在 Python 的多重繼承裡, class
繼承的順序是會影響結果的。如果我們把 Mule
改寫成:
class Mule2(Donkey, Horse):
def __init__(self, is_male):
print('Mule init')
super().__init__(is_male)
def roar(self):
print('Mule: Muuuuleee~~~')
super().roar()
執行結果:
現在應該不難理解為什麼 roar
的執行結果是這樣了吧。
看起來 super
還真是 super 有用啊,能有什麼問題呢我說?
super 麻煩: __init__( ? )
讓我們考慮下面這個函數吧:
def feed_horse(horse):
try:
if horse.is_horse:
horse.speed += 1
else:
horse.speed -= 3
except:
horse.speed -= 5def feed_donkey(donkey):
try:
if donkey.is_donkey:
donkey.speed += 1
else:
donkey.speed -= 3
except:
donkey.speed -= 5
簡單的說,如果分別把馬跟驢丟進個別的函數裡,馬跟驢會被餵草,會跑得比較快,反之就會被操到爆,速度大減。
所以說,既是馬也是驢的騾子,應該不管怎樣都會加速囉?
結果不盡人意:
眼尖的人應該已經發現 Mule.__init__
的不尋常之處, Donkey.__init__
並沒有被呼叫到。為什麼呢?
理解了 super
的運作原理與 mro ,這個疑問也不難回答,原因就出在我們只在 Mule.__init__
中使用 super
而沒有在 Horse.__init__
裡呼叫 super
。
所以, Mule.__init__
中的 super
成功呼叫了 Horse.__init__
,但因為 Horse.__init__
裡沒有 super
,所以 Donkey.__init__
就沒有被呼叫到了, __init__
在這 mro 中就斷掉了。
要修改也不困難,修改完的 code 會長這樣:
class Equus:
def __init__(self, is_male):
pass def run(self):
print(f'I run at speed {self.speed}')
print(f'I\'m {self.gender}')
def roar(self):
print('Equus roars')class Horse(Equus):
def __init__(self, is_male):
print('Horse init')
self.speed = 30
self.gender = 'male' if is_male else 'female'
self.is_horse = True
super().__init__(is_male) def roar(self):
print('Horse: Hee haw~')
super().roar()class Donkey(Equus):
def __init__(self, is_female):
print('Donkey init')
self.speed = 20
self.gender = 'female' if is_female else 'male'
self.is_donkey = True
super().__init__(is_female) def roar(self):
print('Donkey: Hee haw hee hee haw~')
super().roar()class Mule(Horse, Donkey):
def __init__(self, is_male):
print('Mule init')
super().__init__(is_male)
def roar(self):
print('Mule: Muuuuleee~~~')
super().roar()class Mule2(Donkey, Horse):
def __init__(self, is_male):
print('Mule2 init')
super().__init__(is_male)
def roar(self):
print('Mule2: Muuuuleee~~~')
super().roar()
執行看看:
嗯,看來終於正常了。
但又有怪事….
不對啊, is_male
明明是 True
啊,為什麼會變 female
了?
一樣,也是跟 super
與 mro 有關,但這次是出在 Donkey.__init__
。 現在你應該不難理解 Mule.__init__
最後會呼叫 Donkey.__init__
了吧。所以說,當 Mule.__init__
被呼叫時, is_male
會被 pass 給 Donkey.__init__
的 is_female
,這就是你的騾子性別錯亂的原因。
這邊點出了多重繼承裡使用 super
的幾個問題,我簡單列舉如下:
- 因為你要讓所有的物件都被正確地初始化,你必須在所有的 children 與parent classes 都使用
super().__init__
- 由於 mro 的關係,所有的
__init__
都必須要有一樣的 signature 。
第 2 點呼應了我為什麼必須把 Equus
的 __init__
寫得跟 Horse
還有 Donkey
ㄧ樣,明明它什麼都沒做,但我還是必須要寫下它。
(你可以試著拿掉它或改寫它,看看會發生什麼事 : p)
嗯….看起來很麻煩,那有沒有不用 super
又能正確初始化物件的方法?
開玩笑,我們可是在寫自由的 Python 耶,當然有辦法。
以我們的例子來說,可以這樣寫:
class Equus:
def run(self):
print(f'I run at speed {self.speed}')
print(f'I\'m {self.gender}')
def roar(self):
print('Equus roars')class Horse(Equus):
def __init__(self, is_male):
print('Horse init')
self.speed = 30
self.gender = 'male' if is_male else 'female'
self.is_horse = True def roar(self):
print('Horse: Hee haw~')
super().roar()class Donkey(Equus):
def __init__(self, is_female):
print('Donkey init')
self.speed = 20
self.gender = 'female' if is_female else 'male'
self.is_donkey = True def roar(self):
print('Donkey: Hee haw hee hee haw~')
super().roar()class Mule(Horse, Donkey):
def __init__(self, is_male):
print('Mule init')
Horse.__init__(self, is_male)
Donkey.__init__(self, not is_male)
def roar(self):
print('Mule: Muuuuleee~~~')
super().roar()class Mule2(Donkey, Horse):
def __init__(self, is_male):
print('Mule2 init')
Donkey.__init__(self, not is_male)
Horse.__init__(self, is_male)
def roar(self):
print('Mule2: Muuuuleee~~~')
super().roar()
執行結果:
但這也有代價,為此你所有的 __init__
必須是 idenpotent 。白話就是說,不管呼叫一次還是呼叫兩次、先呼叫還是後呼叫,最後物件的狀態應該要ㄧ樣。
但很不幸的是,在我們的例子裡,並沒有滿足這個條件, Mule
物件的起始速度 speed
會受到先呼叫 Horse
還是 Donkey
的影響。這在有用 super
的版本也是ㄧ樣, Mule2
跟 Mule
的起始速度也是不同,儘管概念上來說,對我們來說都是騾子。
所以….我們到底該怎麼用多重繼承啊?
Mixin Pattern
super
與多重繼承還是有一起用還不錯的 case 的。但老實說我如果繼續編例子下去,應該是很難說服人的,不如就看看實際的例子吧! 這裡我舉的例子是 scikit-learn 裡用到的 Mixin Pattern:
看這些例子時,可以注意以下幾點:
- 為什麼 Mixin Types 裡都沒有實作
__init__
? PLS
裡的__init__
如果用到super
,那它會是誰?CCA
裡用super
就不會有問題嗎? 為什麼?
希望現在大家都可以順順地回答這些問題。
總結
在我學習 Python 的過程中,我發現 super
的運作原理與使用時機一直很困擾者我,甚至一度我還認為 super
沒有存在的必要。最後事實證明,Python 的 core developers 比我聰明非常非常多 XD
直到看到 sklearn 的 source code ,綜合之前研究 super
的結果,我才開始能夠理解為什麼 sklearn 的設計會是這樣,也算是看到多重繼承的實際運用。
很久以前研究的,以當時的理解與印象寫了這一篇,沒特別去對新版的 Python 做交叉比對,所以如果有說錯的地方就請大家多多指教囉。
Python 繼承 543 ,到此結束。
Happy Python programming!
Update (2021–03–04)
不少朋友有看這篇,然後討論規避 super
跟 __init__ 問題的方法,所以我想說更新一下我一些想法在這裡。
基本上來說,我想在多重繼承下, super
所產生的問題可以被歸納成:所有繼承樹上的類別,若有重複定義的 method, 則這些 method 必須要有一樣的 signature (除非不同 signature 是刻意且預期內的,但就是你自己要搞清楚整個 calling stack)。
那知道這件事情之後,其實具體來說有幫助的,其實是在你需要實作自己的 library 或 framework 時會需要考慮的,一方面也能用來檢視你的設計有沒有問題。再來也可以看出一些 open source project 的設計有沒有問題就是了 XD