Python - super() 函式與 MRO 詳解
Posted on Oct 6, 2023 in Python 程式設計 - 中階 by Amo Chen ‐ 6 min read
Python 的物件導向程式設計(OOP)有 2 個一定要懂的東西:
- super() 函式
- MRO(Method Resolution Order) / 方法解析順序
如果不懂得這 2 個東西,就無法徹底解放類別(class)的力量,甚至可能導致寫出不夠彈性而且冗長的程式碼。
super() + MRO
= 超級瑪利歐?(誤
本文將從 super()
函式開始講解,說明 Python 的 MRO(Method Resolution Order) ,並介紹 MRO 的特性在實務上的應用。
如果你無法正確回答以下範例結果的執行結果,那麽推薦你看完本文:
class Parent(object):
NAME = 'Parent'
def __str__(self):
return self.NAME
class Child(Parent):
NAME = 'Child'
def __str__(self):
return super().__str__()
c = Child()
print(c)
正確答案為: Child
本文環境
- Python 3
super() 函式介紹
我們都知道物件導向程式設計的重要特點之一是「繼承(Inheritance)」。
透過繼承,我們可以部分改寫(override)父類別的某些屬性、方法,提高程式碼的再利用率與彈性,例如下列 Parent
類別,該類別僅提供一個方法 get_inheritance()
列印其繼承關係:
class Parent(object):
def get_inheritance(self):
return 'Parent'
如果我們有另 1 個 Child
類別繼承 Parent
, 如果要讓 Child
類別正確列印其繼承關係,也就是要讓 get_inheritance()
正確運作的話,那麼我們最毫無技巧的作法就是重寫一遍 get_inheritance()
方法,在裡面寫出 Parent <- Child
, 代表 Child
類別繼承 Parent
類別(如下圖):
class Child(Parent):
def get_inheritance(self):
return 'Parent <- Child'
萬一有很多類別繼承 Parent
類別,或是繼承 Child
類別甚至是 Child
的子類別呢?前述的做法肯定會讓你寫一堆額外的程式碼,甚至缺乏彈性,改一個類別要跟著改其他相關的類別。
那麼,有沒有更好的辦法?
首先看到前述範例的字串 Parent <- Child
中的 Parent
,該字串很明顯來自父類別所定義的方法,如果我們可以在子類別呼叫父類別的方法,那麼 Parent
字串就不用寫死,子類別的方法就寫得可以更彈性。
所以 Python 提供 super() 函式讓我們可以在子類別中呼叫父類別裡定義的方法(即使子類別裡有同名方法,後續會說明為什麼能做到),例如下列範例中的 super().get_inheritance()
, super()
回傳一個 proxy object, 當我們試圖呼叫 1 個方法,它就會代替我們從繼承關係開始向上尋找,直到找到相對應的方法:
class Child(Parent):
def get_inheritance(self):
parent = super().get_inheritance()
return ' <- '.join([parent, 'Child'])
c = Child()
print(c.get_inheritance())
所以上述範例,當我們呼叫 super().get_inheritance()
時,它就會到 Parent
類別尋找有沒有 get_inheritance()
這個方法可以呼叫,並回傳其結果,上述範例執行結果如下,可以看到我們正確呼叫父類別裡的 get_inheritance()
,而且也列印出其正確的結果:
Parent <- Child
如果是 3 layers 的繼承關係,也可以善用 super()
讓每 1 層只要加一點工,例如下列範例同樣在 GrandChild
類別中改寫 get_inheritance()
方法,加上自己獨有的部分即可:
class Parent(object):
def get_inheritance(self):
return 'Parent'
class Child(Parent):
def get_inheritance(self):
parent = super().get_inheritance()
return ' <- '.join([parent, 'Child'])
class GrandChild(Child):
def get_inheritance(self):
parent = super().get_inheritance()
return ' <- '.join([parent, 'GrandChild'])
c = GrandChild()
print(c.get_inheritance())
上述範例執行結果如下,從結果可以看到即使到第 3 層繼承,都不用明確寫明 parent
的值,就能夠把每 1 層的關係在 get_inheritance()
中列印出來:
Parent <- Child <- GrandChild
其原因在於 GrandChild
的 super().get_inheritance()
會呼叫 Child
類別的 get_inheritance()
方法,而 Child
類別的 super().get_inheritance()
又會去呼叫 Parent
類別的 get_inheritance()
方法,最後這些值會串起來,變成上述的結果,如下圖所示。
super() 函式的應用
看到此處,大家應該可以想到 super()
函式很適合用在再包裝(wrapper)的用途上,例如對父類別的方法執行結果做一些加工,變成新的結果!
例如 Icon
類別,當我們想要讓 Icon
也可以有文字提示時,就用 1 個新類別 IconWithText
搭配 super()
函式進行改寫:
class Icon:
def __str__(self):
return '<i class="fa-solid fa-check"></i>'
class IconWithText(Icon):
def __init__(self, text):
self.text = text
def __str__(self):
return super().__str__() + f'<span>{self.text}</span>'
print(IconWithText('Hello'))
上述範例執行結果如下:
<i class="fa-solid fa-check"></i><span>Hello</span>
這種運用 super()
四兩撥千斤類似的用法在 Python OOP 很常見!
super() 函式的誤區
回到文章開頭的範例,應該不少人疑問為什麼正確答案是 Child :
class Parent(object):
NAME = 'Parent'
def __str__(self):
return self.NAME
class Child(Parent):
NAME = 'Child'
def __str__(self):
return super().__str__()
c = Child()
print(c) # Child
當我們在子類別透過 super()
呼叫父類別的方法時,它會有 1 個解析的過程,會先從最近的父類別開始找相對應的方法,如果有的話,就完成解析,如果沒有就往父類別的父類別一路找上去。
但是當執行到父類別的 return self.NAME
時,它這邊跟 super()
函式無關,而是 Attribute Lookup 或 Attribute Search 機制, Python 會先查看 Child
類別裡有沒有 NAME
屬性可以用,而 Child
類別剛好有定義 NAME
屬性,就直接拿來用,因此執行結果是 Child 。
不信的話,可以把 Child
類別的 NAME
屬性刪掉,執行結果就會變為 Parent 。
所以 super()
只管怎麼找到方法,至於找屬性則是另外的機制!
MRO(Method Resolution Order) / 方法解析順序
呼叫類別裡的方法時,解析的過程會從子類別往父類別往上找,如下列範例會找到 A
類別的 run()
:
class A:
def run(self):
print('run')
class B(A):
def __init__(self):
self.run()
B()
不過 Python 是允許多重繼承的,也就是 1 個類別可以繼承多個類別,這種情況下就需要瞭解其解析的規則,這規則稱為 MRO (Method Resolution Order) ,或稱方法解析順序。
所以接下來,談談 MRO 。
首先,以下是 1 個經典的繼承模型:
class A(object):
def method(self):
return 'Class A'
class B(A):
def method(self):
return 'Class B'
class C(A):
def method(self):
return 'Class C'
class D(B, C):
def method(self):
m = super().method()
print(m)
return 'Class D'
d = D()
d.method()
上述範例執行結果如下:
Class B
繼承關係畫成圖的話,如下所示:
從範例程式的執行結果可以看到 D
類別就算同時繼承 B
與 C
類別,最後也只有執行到 B
類別定義的方法而已,這邊可以簡單理解為當繼承多個類別時,解析的順序會從左到右開始。
正常的 MRO 解析過程,在不使用 super()
函式的情況下,例如 class D(B, C)
就會從 D
類別自己開始先找,接著找 B
類別,再來找 C
類別,找到就結束尋找,如果都找不到就再往上一層找。
上述範例則是使用 super()
函式的結果,其解析過程是略過 D
類別,先找 B
類別,再來找 C
類別。
但具體是怎麼回事?我們一步一步說來。
首先,當我們呼叫 super().method()
時,其實等同於呼叫:
super(D, self).method()
先談談 super(D, self)
的第 2 個參數 self
,第 2 個參數會提供給 super()
函式 1 個解析順序,也就是 MRO 。
想知道 MRO 的話,可以用以下 2 種方式取得:
<類別名稱>.__mro__
<類別名稱>.mro()
例如:
>>> print(D.__mro__)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
或
>>> print(D.mro())
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
可以看到 D
類別的 MRO 是 D → B → C → A → object 。
p.s. object
類別是 Python 所有類別之母,所以最後都是 object
super()
有了第 2 個參數,就知道 MRO 是什麼以及怎麼找方法。
至於 super()
第 1 個參數則代表要從 MRO 的哪裡開始找,例如 super(D, self)
就代表要從 B → C → A → object 開始一路找上去,這就是為什麼即使 D
類別裡有個同名的方法時,也不會被執行的原因!
看到這裡,大家應該知道怎麼控制 super()
函式,讓它執行特定父類別的方法囉!也就是控制第 1 個參數,讓 super()
從 MRO 裡的特定位置開始找,舉下列述範例為例, D
類別的 MRO 為 D → B → C → A → object, 在不控制 super()
函式的情況下,如果呼叫 super().method()
會從 B → C → A → object 順藤摸瓜尋找 method()
方法,由於 C
類別有定義 method()
, 所以最後執行結果為 Class C
:
class A(object):
def method(self):
return 'Class A'
class B(A): pass
class C(A):
def method(self):
return 'Class C'
class D(B, C):
def method(self):
m = super().method()
print(m)
return 'Class D'
d = D()
d.method() # result: Class C
如果我們想要讓 super()
去呼叫 A
類別的方法,只要將 super()
改為 super(C, self)
即可,因為它的解析就會變成 A → object :
class A(object):
def method(self):
return 'Class A'
class B(A): pass
class C(A):
def method(self):
return 'Class C'
class D(B, C):
def method(self):
m = super(C, self).method()
print(m)
return 'Class D'
d = D()
d.method() # Class A
大家可以試看看上述程式碼!
多重繼承與 MRO 的實務應用
如果能正確理解 MRO 的觀念,加上 Python 的多重繼承,就能夠設計稱為 Mixin 的類別,這種類別特點就像樂高一樣,可以用多重繼承的方式賦予類別各式各樣的功能,舉下列範例為例,該範例設計 2 個 Mixin 類別,分別是 FlyMixin
與 DiveMixin
, SuperSubmarine
類別則透過多重繼承這 2 個 Mixin 獲得飛行(fly)與潛水(dive)的能力:
class FlyMixin(object):
def fly(self):
print('Fly!')
class DiveMixin(object):
def dive(self):
print('Dive!')
class SuperSubmarine(FlyMixin, DiveMixin): pass
s = SuperSubmarine()
s.fly()
s.dive()
上述範例執行結果如下:
Fly!
Dive!
因為 MRO, 所以 SuperSubmarine
在呼叫 fly()
時,會先找到 FlyMixin
裡的 fly()
, 在呼叫 dive()
時,則因為 FlyMixin
沒有定義 dive()
,所以往後找到 DiveMixin
裡的 dive()
。
實務上,Django 的 View 就提供很多 Mixin 類別可以使用,其原理就是如此簡單。
同場加映——先呼叫 super().__init__()
還是後呼叫 super().__init__()
?
下列是先呼叫 super().__init__()
的範例,可以看到 B().name
是正確的 b
:
class A:
def __init__(self):
self.name = 'a'
class B(A):
def __init__(self):
super().__init__()
self.name = 'b'
print(B().name) # b
下列是後呼叫 super().__init__()
的範例,可以看到 B().name
是不正確的 a
:
class A:
def __init__(self):
self.name = 'a'
class B(A):
def __init__(self):
self.name = 'b'
super().__init__()
print(B().name)
這樣先後呼叫的區別,也會造成執行結果不同,有時候腦袋不清楚,就會沒注意到,要特別注意這種情況!
總結
super()
函式的運用是邁向高手的重要關卡,在此之前正確理解 MRO 更是絕對要做的基本功,結合 2 者,將可以在 Python OOP 世界裡更加自在。
以上!
Happy Coding!
References
Python’s super() considered super!
Django - Class-based views mixins