Python 繼承 543

Dboy Liao
17 min readMay 13, 2018

--

相信寫 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 方法,但同時又想使用到 Horseroar ,你可以這樣寫:

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 methodstatic 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()

騾子是馬跟驢的混種,我們希望牠的行為可以混雜著兩者個特性。

執行的結果:

有沒有覺得哪裡怪怪的? 提示: init

很好,這個騾子叫的方式果然如我們所希望的,同時混雜著馬跟驢的叫聲。

這個結果簡單的流程可以想成這樣:

  1. Mule.roar 被呼叫, print('Mule: ...') 被執行
  2. Mule.roar 中的 super().roar() 被執行, Horse.roar 被呼叫, print('Horse: ...') 被執行
  3. 同理, Donkey.roar 也被執行 (因為 Horse.roar 裡的 super
  4. 最後 Equusroar 被呼叫

看到 3 你或許會覺得奇怪,為什麼在 Donkeyroar 裡的 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.roarsuper 再次被呼叫,建立了 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 -= 5
def 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 的幾個問題,我簡單列舉如下:

  1. 因為你要讓所有的物件都被正確地初始化,你必須在所有的 children 與parent classes 都使用 super().__init__
  2. 由於 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 的版本也是ㄧ樣, Mule2Mule 的起始速度也是不同,儘管概念上來說,對我們來說都是騾子。

所以….我們到底該怎麼用多重繼承啊?

Mixin Pattern

super 與多重繼承還是有一起用還不錯的 case 的。但老實說我如果繼續編例子下去,應該是很難說服人的,不如就看看實際的例子吧! 這裡我舉的例子是 scikit-learn 裡用到的 Mixin Pattern:

看這些例子時,可以注意以下幾點:

  1. 為什麼 Mixin Types 裡都沒有實作 __init__?
  2. PLS 裡的 __init__ 如果用到 super ,那它會是誰?
  3. 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

參考資料

--

--

Dboy Liao
Dboy Liao

Written by Dboy Liao

Code Writer, Math Enthusiast and Data Scientist, yet.

Responses (2)