在支援物件導向的程式語言中,如果能實現類別繼承,子類別需要使用父類別的方法時,會提供super之類的關鍵字(例如Java)或語法糖(例如JavaScript),Python則是透過super類別,在建立super實例後,進一步地指定父類別方法名稱並呼叫,來達到呼叫父類別方法之目的。

呼叫父類別方法的方式

在絕大多數的情況下,建立super實例時,並不需要(也不建議)指定任何引數,單看程式語法super().method()而言,除了super旁多了括號,使用上與其他語言的super關鍵字無異,如果撰寫程式時,僅使用單一繼承,以無引數方式建立super實例,super可以直接認定為「父」類別。

其實,不建立super實例,我們也可以呼叫父類別的方法,畢竟Python的實例方法,就像黏在類別上的函式,只不過以實例呼叫方法時,方法的第一個參數總是綁定實例本身,因此,若父類別為P,super().method()也可以寫成P.method(self),只要記得第一個參數傳入實例本身就可以了,只不過子類別中寫死了父類別名稱,如果父類別要更改名稱,子類別中相對應的名稱就必須修改,對於後續的維護是個麻煩。

然而,有些語言確實是這麼做的,例如Java的類別在實現多個介面時,若介面有同名的預設方法,會引發編譯錯誤,類別必須明確地實作該方法,並且於方法中指定要使用哪個介面的預設方法。

在Python實作多重繼承的時候,這個方法看似還有個優點,那就是:可以自行決定父類方法的呼叫順序。只不過這種方式依賴在開發者,可能會造成每個開發者面對多重繼承時,呼叫父類方法的方式都不相同,另一方面,多重繼承的情況可能更複雜,像是形成菱形繼承。

如果P1、P2繼承Base,S依序繼承P1、P2,S在自身方法中呼叫了P1.method(self)與P2.method(self),而P1、P2又各自在方法中呼叫了Base.method(self),最終會導致Base的method被呼叫兩次。事實上,別以為基於Mixin的概念來使用多重繼承,就不會有這類問題,畢竟Mixin的來源可能會有同名的方法。

在《Effective Python》第二版〈做法40〉所談到的,就是這類情況,並建議採用(無引數)super實例,解決父類方法被重複呼叫的問題!

多重繼承與MRO

簡單來說,super實例提供了一個標準方式,解決多重繼承下方法的尋找順序問題,而不是依賴在開發者的各自實作,這個標準方式就是基於MRO(Method Resolution Order),具體而言,就是類別的__mro__清單,我們也可以透過類別的mro類別方法取得。

python直譯器會以開發者定義的類別繼承順序,自動生成MRO。以方才談到的多重繼承架構而言,S的__mro__清單會是S、P1、P2、Base、object,而MRO是屬性尋找的順序,如果P1、P2都定義了xxx方法,那麼,呼叫S實例的xxx方法,所找到的會是P1的xxx方法。

就方才談到的多重繼承架構而言,我們可以改寫為S在自身方法中呼叫super().method(),而P1、P2又各自在方法中呼叫了super().method(),於是,最後執行結果構成了基於MRO的呼叫堆疊,也就是,依序呼叫P1的method、P2的method、Base的method,因此,最後Base的method只會執行一次,解決了方法被重複呼叫的問題。

另一方面,開發者如果想知道多重繼承下父類方法的呼叫順序,可以看看類別實現繼承時的順序,或者直接查看__mro__就知道了。開發者面對多重繼承時,透過這種作法,能避免呼叫父類方法的方式都不相同的問題。

以super()建立super實例,以呼叫父類別方法的方式,在類別方法(@classmethod標註的方法)也行得通。這意謂著,我們可以在__new__中,以super().__new__(...)來呼叫父類的__new__方法,就如同在__init__中,我們可以用super().__init__(...)來呼叫父類的__init__方法。

指定super的引數

在絕大多數的情況下,通常會建議使用無引數方式建立super實例。然而,super確實是可以指定引數來建立實例,其中一種方式是super(clz,obj),這種方式必須滿足isinstance(obj,clz),若以此形式呼叫時,會使用type(obj)的__mro__作為尋找方法的依據,這時super(clz,obj)的意思,是在type(obj)的__mro__,尋找clz的「上」一個類別(因為super也有over、above之意)。

如果在Python的REPL環境使用help(super),我們可以看到無引數的呼叫方式等同於super(__class__,<first argument>),<first argument>是指super呼叫時所在方法的第一個引數,若就實例方法而言,就是self,亦即等同於super(__class__,self),這也說明方才多重繼承的情況下,為何會是P1、P2而後Base的呼叫順序。

super指定引數的另一個形式是super(clz,clz2),必須滿足issubclass(clz2,clz),這時,我們會使用clz的__mro__作為尋找方法的依據,也就是在clz的__mro__,尋找clz2的「上」一個類別。

類別方法的第一個引數會綁定類別本身,若在類別方法中,使用無引數方式建立super實例,方才提到的<first argument>就是類別本身,亦即相當於super(clz,clz2)的呼叫形式,這也就是為何以無引數方式去建立super實例時,在類別方法中也行得通的原因。

也就是說,super(clz,obj)或super(clz,clz2)本質上是相同的,基於第二個引數可取得的__mro__,尋找第一個引數的「上」一個類別。

開發者可能會問:靜態方法(以@staticmethod標註的方法)呢?就Python而言,靜態方法第一個參數不會綁定物件,由於靜態方法只是將類別作為名稱空間罷了,並沒有繼承的概念,開發者如果要明確指定是使用哪個類別的哪個靜態方法,就去指定要哪個模組的哪個函式,其實是一樣的道理。

建立多重繼承時的規範

super其實還有個super(clz)呼叫方式,然而沒有實用的情境,簡單來說,這個方式建立的super實例,尚未綁定可參考__mro__的物件,後續我們必須進一步指定綁定可取得__mro__的物件,才能用來尋找屬性。

雖然沒有實用的情境,能夠認識super(clz)呼叫方式,仍有助於瞭解super、MRO與方法之間的關係,想多了解這部分,可進一步查看〈super綁定/未綁定〉

方才的文件也談到了如何自定義類別來模擬super(clz,obj),我們從中也能瞭解到:有引數的super實例建立方式,也是給予開發者另一種彈性,如果預設的MRO順序、尋找父類別方法,並不符合需求時,就可以在建立super時指定引數。

當然必要時,如同方才一開始,自行指定父類別與呼叫順序的方式,基本上也是可以,然而,此時最好以文件明確地建立規範,讓其他開發者可以遵循或查閱。

某些程度上,這也是Python提供super之目的,因為,繼承本身就會引來複雜,而且,多重繼承更是複雜,使用時,務必要有標準方式,無論那個標準是語言內建、文件規範,或甚至是其他第三方工具的輔助!

專欄作者

熱門新聞

Advertisement