Python 繼承543 - Dboy Liao
文章推薦指數: 80 %
相信寫OOP 的人對於繼承這個概念應該不陌生,Python 身為一個支援OOP 的語言,自然 ... class MiniHorse(Horse): def __init__(self, is_male, leg_length): super().
GetunlimitedaccessOpeninappHomeNotificationsListsStoriesWritePython繼承543相信寫OOP的人對於繼承這個概念應該不陌生,Python身為一個支援OOP的語言,自然也支援繼承。
由於Python2已經慢慢被淘汰,下面的例子會以Python3為準做說明。
Python繼承極簡介classEquus:def__init__(self,is_male):passdefrun(self):print(f'Irunatspeed{self.speed}')print(f'I\'m{self.gender}')classHorse(Equus):def__init__(self,is_male):print('Horseinit')self.speed=30self.gender='male'ifis_maleelse'female'self.is_horse=Truedefroar(self):print('Heehaw~')classDonkey(Equus):def__init__(self,is_female):print('Donkeyinit')self.speed=20self.gender='female'ifis_femaleelse'male'self.is_donkey=Truedefroar(self):print('Heehawheeheehaw~')這邊我們有兩個class,馬(Horse)與驢子(Donkey),都是馬屬(Equus),馬屬的東(ㄨˋ)西(ㄐㄧㄢˋ)都會跑(run)。
這邊我們可以看到我們利用繼承達到程式碼的復用,馬跟驢子共用了馬屬的runmethod,而馬跟驢有不同的叫聲(roar)。
到目前為止,相信身為Python工程師,應該都不陌生。
接著,讓我們聊聊super吧。
super的美麗與哀愁super美麗假設我們現在想寫個MiniHorse繼承Horse,並覆寫roar方法,但同時又想使用到Horse的roar,你可以這樣寫:classMiniHorse(Horse):def__init__(self,is_male,leg_length):super().__init__(is_male)self.leg_length=leg_lengthdefroar(self):super().roar()print('miniminimini')這邊是super的基本用法,分別在__init__裡用super去使用Horse。
或許會想,所以super就是Horse?這邊比較一下不用super的寫法:classMiniHorse(Horse):def__init__(self,is_male):Horse.__init__(self,is_male)self.leg_length=leg_lengthdefroar(self):Horse.roar(self)print('miniminimini')前者是所謂的boundmethod的呼叫方式,後者是用unboundmethod的呼叫方式,在這個例子裡兩者等價。
(有興趣的讀者再自行google吧,建議關鍵字有unbound、boundmethod與staticmethod)看看super的說明:所以說,super是一種type,用於建立super物件。
在第一種寫法裡,我們用super()建立一個bounded的superobject,因此可以使用Horse的物件方法。
這也是super最常見的用法,用於呼叫superclass的方法。
可是也可以不用super做到一樣的事,那到底要它幹嘛?多重繼承(multipleinheritance)要super幹嘛?想回答這個問題,就不得不聊聊Python的多重繼承。
這可是連Java都做不到的事呢!(Java必須使用interface)不過這邊先讓我賣個關子,讓我用roar來體現super的用途。
之後會回頭講講__init__,會有趣很多。
讓我們稍稍改寫一下上面的範例classEquus:def__init__(self,is_male):passdefrun(self):print(f'Irunatspeed{self.speed}')print(f'I\'m{self.gender}')defroar(self):print('Equusroars')classHorse(Equus):def__init__(self,is_male):print('Horseinit')self.speed=30self.gender='male'ifis_maleelse'female'self.is_horse=Truedefroar(self):print('Horse:Heehaw~')super().roar()classDonkey(Equus):def__init__(self,is_female):print('Donkeyinit')self.speed=20self.gender='female'ifis_femaleelse'male'self.is_donkey=Truedefroar(self):print('Donkey:Heehawheeheehaw~')super().roar()有發現我改了什麼嗎?(=w=)b接著,我們使用多重繼承實作騾子(Mule):classMule(Horse,Donkey):def__init__(self,is_male):print('Muleinit')super().__init__(is_male)defroar(self):print('Mule:Muuuuleee~~~')super().roar()騾子是馬跟驢的混種,我們希望牠的行為可以混雜著兩者個特性。
執行的結果:有沒有覺得哪裡怪怪的?提示:init很好,這個騾子叫的方式果然如我們所希望的,同時混雜著馬跟驢的叫聲。
這個結果簡單的流程可以想成這樣: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的superclass不是應該是Equus嗎?MROandC3Linearization嚴格來說,說super()是用來使用superclass的方法並不是一個正確的說法。
比較正確的說法應該是它會建立一個使用mro中下一個type的boundedproxyobject。
(WTF...)沒關係,一個一個來。
首先是mro。
呼叫Mule.mro,你應該會看到:Mule.mro的說明:簡單的說,mro是在多重繼承的時候,決定該使用哪一個parentclass的method的搜尋路徑。
而這個路徑,是由C3Linearization這個演算法所決定。
隨便google一下就會有一堆C3Linearization的說明,這邊就不贅述了。
以這邊的Mule來說,因為是宣告成classMule(Horse,Donkey):...,所以mro中,Mule之後先是Horse,再來是Donkey。
所以整個Mule.roar被呼叫時的super被bound成Horse,所以這時候的super().roar會是Horse的instancemethodcall;然後在Horse.roar裡super再次被呼叫,建立了Donkey的boundproxyobject,所以這時的super().roar會變成Donkey.roar的instancemethodcall;最後,Donkey.roar裡的super就被proxy到Equus去了。
這就是Python使用super實現多重繼承的方式。
所以說,在Python的多重繼承裡,class繼承的順序是會影響結果的。
如果我們把Mule改寫成:classMule2(Donkey,Horse):def__init__(self,is_male):print('Muleinit')super().__init__(is_male)defroar(self):print('Mule:Muuuuleee~~~')super().roar()執行結果:現在應該不難理解為什麼roar的執行結果是這樣了吧。
看起來super還真是super有用啊,能有什麼問題呢我說?super麻煩:__init__(?)讓我們考慮下面這個函數吧:deffeed_horse(horse):try:ifhorse.is_horse:horse.speed+=1else:horse.speed-=3except:horse.speed-=5deffeed_donkey(donkey):try:ifdonkey.is_donkey:donkey.speed+=1else:donkey.speed-=3except: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會長這樣:classEquus:def__init__(self,is_male):passdefrun(self):print(f'Irunatspeed{self.speed}')print(f'I\'m{self.gender}')defroar(self):print('Equusroars')classHorse(Equus):def__init__(self,is_male):print('Horseinit')self.speed=30self.gender='male'ifis_maleelse'female'self.is_horse=Truesuper().__init__(is_male)defroar(self):print('Horse:Heehaw~')super().roar()classDonkey(Equus):def__init__(self,is_female):print('Donkeyinit')self.speed=20self.gender='female'ifis_femaleelse'male'self.is_donkey=Truesuper().__init__(is_female)defroar(self):print('Donkey:Heehawheeheehaw~')super().roar()classMule(Horse,Donkey):def__init__(self,is_male):print('Muleinit')super().__init__(is_male)defroar(self):print('Mule:Muuuuleee~~~')super().roar()classMule2(Donkey,Horse):def__init__(self,is_male):print('Mule2init')super().__init__(is_male)defroar(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與parentclasses都使用super().__init__由於mro的關係,所有的__init__都必須要有一樣的signature。
第2點呼應了我為什麼必須把Equus的__init__寫得跟Horse還有Donkeyㄧ樣,明明它什麼都沒做,但我還是必須要寫下它。
(你可以試著拿掉它或改寫它,看看會發生什麼事:p)嗯….看起來很麻煩,那有沒有不用super又能正確初始化物件的方法?開玩笑,我們可是在寫自由的Python耶,當然有辦法。
以我們的例子來說,可以這樣寫:classEquus:defrun(self):print(f'Irunatspeed{self.speed}')print(f'I\'m{self.gender}')defroar(self):print('Equusroars')classHorse(Equus):def__init__(self,is_male):print('Horseinit')self.speed=30self.gender='male'ifis_maleelse'female'self.is_horse=Truedefroar(self):print('Horse:Heehaw~')super().roar()classDonkey(Equus):def__init__(self,is_female):print('Donkeyinit')self.speed=20self.gender='female'ifis_femaleelse'male'self.is_donkey=Truedefroar(self):print('Donkey:Heehawheeheehaw~')super().roar()classMule(Horse,Donkey):def__init__(self,is_male):print('Muleinit')Horse.__init__(self,is_male)Donkey.__init__(self,notis_male)defroar(self):print('Mule:Muuuuleee~~~')super().roar()classMule2(Donkey,Horse):def__init__(self,is_male):print('Mule2init')Donkey.__init__(self,notis_male)Horse.__init__(self,is_male)defroar(self):print('Mule2:Muuuuleee~~~')super().roar()執行結果:但這也有代價,為此你所有的__init__必須是idenpotent。
白話就是說,不管呼叫一次還是呼叫兩次、先呼叫還是後呼叫,最後物件的狀態應該要ㄧ樣。
但很不幸的是,在我們的例子裡,並沒有滿足這個條件,Mule物件的起始速度speed會受到先呼叫Horse還是Donkey的影響。
這在有用super的版本也是ㄧ樣,Mule2跟Mule的起始速度也是不同,儘管概念上來說,對我們來說都是騾子。
所以….我們到底該怎麼用多重繼承啊?MixinPatternsuper與多重繼承還是有一起用還不錯的case的。
但老實說我如果繼續編例子下去,應該是很難說服人的,不如就看看實際的例子吧!這裡我舉的例子是scikit-learn裡用到的MixinPattern:MixinclassesinsklearnPartialLeastSquareandCanonicalCorrelationAnalysis.看這些例子時,可以注意以下幾點:為什麼MixinTypes裡都沒有實作__init__?PLS裡的__init__如果用到super,那它會是誰?CCA裡用super就不會有問題嗎?為什麼?希望現在大家都可以順順地回答這些問題。
總結在我學習Python的過程中,我發現super的運作原理與使用時機一直很困擾者我,甚至一度我還認為super沒有存在的必要。
最後事實證明,Python的coredevelopers比我聰明非常非常多XD直到看到sklearn的sourcecode,綜合之前研究super的結果,我才開始能夠理解為什麼sklearn的設計會是這樣,也算是看到多重繼承的實際運用。
很久以前研究的,以當時的理解與印象寫了這一篇,沒特別去對新版的Python做交叉比對,所以如果有說錯的地方就請大家多多指教囉。
Python繼承543,到此結束。
HappyPythonprogramming!Update(2021–03–04)不少朋友有看這篇,然後討論規避super跟__init__問題的方法,所以我想說更新一下我一些想法在這裡。
基本上來說,我想在多重繼承下,super所產生的問題可以被歸納成:所有繼承樹上的類別,若有重複定義的method,則這些method必須要有一樣的signature(除非不同signature是刻意且預期內的,但就是你自己要搞清楚整個callingstack)。
那知道這件事情之後,其實具體來說有幫助的,其實是在你需要實作自己的library或framework時會需要考慮的,一方面也能用來檢視你的設計有沒有問題。
再來也可以看出一些opensourceproject的設計有沒有問題就是了XD參考資料BlogpostbyGaëlPegliascoC3-Linearization:WikiMorefromDboyLiaoFollowCodeWriter,MathEnthusiastandDataScientist,yet.Lovepodcastsoraudiobooks?Learnonthegowithournewapp.TryKnowableAboutHelpTermsPrivacyGettheMediumappGetstartedDboyLiao436FollowersCodeWriter,MathEnthusiastandDataScientist,yet.FollowHelpStatusWritersBlogCareersPrivacyTermsAboutKnowable
延伸文章資訊
- 1Supercharge Your Classes With Python super()
__init__() of the superclass ( Square ) will be called automatically. super() returns a delegate ...
- 2多重繼承 - iT 邦幫忙::一起幫忙解決難題,拯救IT 人的一天
從寫程式到脫離菜雞的歷練(以python為主的資處與檔案權限) 系列第18 篇 ... __init__() #super調用每個class內指定__init__ d = D() d.fc_a(...
- 3Python 繼承543 - Dboy Liao
相信寫OOP 的人對於繼承這個概念應該不陌生,Python 身為一個支援OOP 的語言,自然 ... class MiniHorse(Horse): def __init__(self, is_...
- 4用super 來讓父系幫助你· Introducing python - iampennywu
只要說super(). >>> class 父類別名稱(): def __init__(self, name): self.name = name # 注意以下「子類別」內的__init__()...
- 5Understanding Python super() with __init__() methods
super() lets you avoid referring to the base class explicitly, which can be nice. But the main ad...