super 與 mro
May 17, 2022在〈共同行為與 is a〉看過 super
的使用,實際上 super
可以指定引數,只不過絕大多數的情況下,只需要也建議以無引數的方式使用 super
。
在〈多重繼承與 mixin〉談過 __mro__
,super
真正的作用,是用來簡化多重繼承時尋找方法的一個方案,借用 __mro__
來控制解析的順序。
不使用 super 的話
在其他語言中,特別是只能單一繼承的語言中,super
常作為關鍵字存在,作用就是呼叫父類別方法,看到 super
,直接想成是「父類別」就可以了。
在 Python 中,super
經常被用來呼叫父類別方法,如果撰寫程式時僅使用單一繼承,以無引數方式呼叫 super
,看到 super
,也可以直接想成是「父類別」。
其實不使用 super
,也可以呼叫父類別的方法,例如〈共同行為與 is a〉中 SwordsMan
、Magician
使用 super
的範例,也可以寫成:
class SwordsMan(Role):
def fight(self):
print('揮劍攻擊')
def __str__(self):
return f'SwordsMan{Role.__str__(self)}'
class Magician(Role):
def fight(self):
print('魔法攻擊')
def cure(self):
print('魔法治療')
def __str__(self):
return f'Magician{Role.__str__(self)}'
初始化時的範例,也可以如下撰寫:
class SwordsMan(Role):
def __init__(self, name, level, blood):
Role.__init__(self, name, level, blood)
...其他程式碼
Python 的方法,基本上可視為以類別為名稱空間的函式,只要記得第一個參數傳入實例本身就可以了,只不過以上的方式,在子類別中寫死了父類別名稱,如果父類別更名了,子類別中相對應的名稱就得修改,對維護上是個麻煩。
在多重繼承的時候,這個方法看似有個優點,可以自行決定父類方法的呼叫順序,例如〈多重繼承與 mixin〉談過多重繼承時,若繼承的多個父類別有同名方法,也依 __mro__
的順序尋找方法:
>>> class P1:
... def mth(self):
... print('P1 mth')
...
>>> class P2:
... def mth(self):
... print('P2 mth')
...
>>> class S(P1, P2):
... pass
...
>>> s = S()
>>> s.mth()
P1 mth
>>>
問題來了,如果 S
重新定義了 mth
,想呼叫父類別方法 mth
時,該呼叫 P1
還是 P2
的呢?如果兩個都想呼叫,那要先呼叫 P1
還是 P2
的呢?
雖然 P1
、P2
沒有定義實例的屬性,看來像是個 Mixin 的情境,不過 Mixin 確實會遇到這類問題,解決的方式之一是,自行指定父類別:
>>> class P1:
... def mth(self):
... print('P1 mth')
...
>>> class P2:
... def mth(self):
... print('P2 mth')
...
>>> class S(P1, P2):
... def mth(self):
... P1.mth(self)
... P2.mth(self)
...
>>> s = S()
>>> s.mth()
P1 mth
P2 mth
>>>
以上範例 S
的呼叫順序,基本上是按照多重繼承時 P1
、P2
的指定順序,這種方式也可以用於 __init__
,也就是子類實例建立時,可自行指定父類的 __init__
與順序,來定義實例的初始化流程。
有些語言確實是這麼做的,例如 Java 的類別在實現多個介面時,若介面有同名的預設方法,類別必須明確地實作該方法,並且於方法中指定要使用哪個介面的預設方法。
只不過這種方式依賴在開發者,可能會造成每個開發者面對多重繼承時,呼叫父類方法的方式都不相同,另一方面,多重繼承的情況可能更複雜,像是形成菱形繼承:
>>> class Base:
... def mth(self):
... print('base mth')
...
>>> class P1(Base):
... def mth(self):
... Base.mth(self)
... print('P1 mth')
...
>>> class P2(Base):
... def mth(self):
... Base.mth(self)
... print('P2 mth')
...
>>> class S(P1, P2):
... def mth(self):
... P1.mth(self)
... P2.mth(self)
...
>>> s = S()
>>> s.mth()
base mth
P1 mth
base mth
P2 mth
>>>
在 P1
、P2
的 mth
方法中,同樣以 Base
呼叫了父類別的 mth
方法,就最後執行結果來看,Base
的 mth
被呼叫了兩次,這並不正確。
另一方面,多重繼承下,S(P1, P2)
其實呈現了繼承的順序關係,父類方法的執行順序,是要以 Base
、P1
、P2
的順序,還是以 Base
、P2
、P1
的順序呢?如果這種順序關係仍然依賴在開發者各自的定義上,那麼在維護時,確認執行順序,將會是一種艱難的任務。
實例方法與 super
若使用無引數的 super
來實作方才的範例,會有什麼結果呢?
>>> class Base:
... def mth(self):
... print('base mth')
...
>>> class P1(Base):
... def mth(self):
... super().mth()
... print('P1 mth')
...
>>> class P2(Base):
... def mth(self):
... super().mth()
... print('P2 mth')
...
>>> class S1(P1, P2):
... def mth(self):
... super().mth()
...
>>> class S2(P2, P1):
... def mth(self):
... super().mth()
...
>>> s1 = S1()
>>> s1.mth()
base mth
P2 mth
P1 mth
>>>
>>> s2 = S2()
>>> s2.mth()
base mth
P1 mth
P2 mth
>>>
使用無引數的 super
,Base
的 mth
只會被執行一次,至於順序上,S1(P1, P2)
的話,順序會是 Base
、P2
、P1
,若是 S2(P2, P1)
的話,順序會是 Base
、P1
、P2
,這個順序是?
這個順序其實就是 __mro__
的順序,可以使用類別方法 mro
來得知順序:
>>> S1.mro()
[<class '__main__.S1'>, <class '__main__.P1'>, <class '__main__.P2'>, <class '__main__.Base'>, <class 'object'>]
>>> S2.mro()
[<class '__main__.S2'>, <class '__main__.P2'>, <class '__main__.P1'>, <class '__main__.Base'>, <class 'object'>]
>>>
為什麼是 __mro__
的順序呢?來看看 super
的簽署:
>>> help(super)
Help on class super in module builtins:
class super(object)
| super() -> same as super(__class__, <first argument>)
| super(type) -> unbound super object
| super(type, obj) -> bound super object; requires isinstance(obj, type)
| super(type, type2) -> bound super object; requires issubclass(type2, type)
...略
可以看到,無引數方式呼叫 super
,相當於 super(__class__, <first argument>)
,__class__
就是指當時正在定義的類別,而 <first argument>
是指 super
呼叫時所在方法的第一個引數。
如果你在實例方法方法中以無引數呼叫 super
,<first argument>
就是 self
,也就是相當於 super(__class__, self)
,這時就是 super(type, obj)
的形式,self
是當時所在類別 __class__
的實例,符合 isinstance(obj, type)
的要求。
以 super(type, obj)
的形式呼叫時,其實是使用 type(obj)
的 __mro__
作為尋找方法的依據,type
必須在 __mro__
上,而 super(type, obj)
的意思是,在 type(obj)
的 __mro__
上,尋找 type
的上一個類別(super 也有 over、above 之意)。
因此回過頭來看方才的範例:
class Base:
def mth(self):
print('base mth')
class P1(Base):
def mth(self):
super().mth()
print('P1 mth')
class P2(Base):
def mth(self):
super().mth()
print('P2 mth')
class S1(P1, P2):
def mth(self):
super().mth()
s1 = S1()
s1.mth()
S1
的 super().mth()
,會尋找 type(self)
的 __mro__
中,S1
的上一個類別的 mth
,也就是 P1
的 mth
,因為 self
就是 s1
參考的實例,也就是使用 type(s1)
的 __mro__
,如果找到上一個類別的 mth
,會以綁定方法傳回,綁定的對象 super
指定的第二個引數,也就是 s1
參考的實例,因此執行時,方法的 self
就是 s1
參考的實例。
接續方才的解析,P1
的 super().mth()
,會尋找 type(s1)
的 __mro__
中,P1
的上一個類別的 mth
,也就是 P2
,而 P2
的 super().mth()
,會尋找 type(s1)
的 __mro__
中,P2
的上一個類別的 mth
,也就是 Base
。
因此最後,Base
的 mth
只會被執行一次,也才會看到 base mth、P2 mth、P1 mth 的執行順序,依照同樣的方式,你可以自行解析 s2.mth()
時,為何會是 base mth、P1 mth、P2 mth 的順序。
簡單來說,super
提供了一個標準方式,解決多重繼承下方法的尋找順序問題,而不是依賴在開發者的各自實作,這個標準方式,就是基於 super
第二個引數可提供的 __mro__
,尋找第一個引數的上一個類別。
類別方法與 super
那麼 super(type, type2)
是怎麼回事呢?來看看以下的範例:
>>> class Base:
... @classmethod
... def mth(clz):
... print('base mth')
...
>>> class P1(Base):
... @classmethod
... def mth(clz):
... super().mth()
... print('P1 mth')
...
>>> class P2(Base):
... @classmethod
... def mth(clz):
... super().mth()
... print('P2 mth')
...
>>> class S1(P1, P2):
... @classmethod
... def mth(clz):
... super().mth()
...
>>> class S2(P2, P1):
... @classmethod
... def mth(clz):
... super().mth()
...
>>> S1.mth()
base mth
P2 mth
P1 mth
>>> S2.mth()
base mth
P1 mth
P2 mth
>>>
這次在 mth
上,都標註了 @classmethod
,也就是 mth
是個類別方法,第一個參數接受類別,mth
中使用無引數的 super
呼叫,相當於 super(__class__, <first argument>)
,現在 <first argument>
會是第一個參數接受的類別,也就是相當於 super(type, type2)
,以 S1.mth()
來說,就是 super(S1, S1)
的意思,這還是會讓 issubclass(type2, type)
成立,因為 issubclass(S1, S1)
是 True
。
其實 super
的作用就如方才提到的,基於 super
第二個引數可提供的 __mro__
,尋找第一個引數的上一個類別,就類別方法而言,無引數呼叫 super
,就是 super(type, type2)
形式,也就是基於 super
第二個引數 type2
的 __mro__
,尋找第一個引數 type
的上一個類別。
因此解析 S1.mth()
的方式,與方才解析 s1.mth()
就類似了,只不過 super
綁定的對象,是 super
指定的第二個引數,就類別方法而言,就是傳入的類別,因此解析過程中,clz
都是 S1
,也就是看 S1
的 __mro__
,尋找 type
的上一個類別,S2.mth()
的話,就都是看 S2
的 __mro__
了。
絕大多數的情況下,你應該使用無引數的方式呼叫 super
,以標準方式解析屬性,也就是基於 __mro__
的順序來尋找屬性。
然而,確實仍然可以透過 super
有引數的呼叫方式,來決定該怎麼解析屬性,甚至是直接指定父類別名稱,決定該怎麼呼叫父類方法,雖然這是不建議的做法。
super 綁定/未綁定
方才看到還有個 super(type)
的使用方式,說明上說是 unbound super object,未綁定?這會讓人以為是不是與靜態方法有關,不過 super(type)
的使用方式,跟靜態方法沒有關係。
就 Python 而言,靜態方法第一個參數不會綁定物件,靜態方法只是將類別作為名稱空間罷了,沒有繼承的概念,你要明確指定是使用 A
類別的 xxx
靜態方法或 B
類別的 xxx
方法,這就跟你會指定要 a
模組 xxx
函式或 b
模組的 xxx
函式,是一樣的道理。
首先要知道的是,super
其實是個類別,以 super(type, obj)
、super(type, type2)
形式(super()
看是在實例方法或類別方法中呼叫,會涵蓋這兩者)呼叫的話,會建立 super
的實例,如果在第二個引數可提供的 __mro__
上找到 type
上一個類別,後續呼叫的屬性存在時,會傳回綁定方法,方法的第一個參數,會綁定為 super
第二個引數指定的物件。
例如,實例方法的情況會是:
>>> class Base:
... def mth(self):
... print('base mth')
...
>>> class P(Base):
... pass
...
>>> p = P()
>>> super(P, p).mth
<bound method Base.mth of <__main__.P object at 0x00000157A0910B20>>
>>>
類別方法的情況會是:
>>> class Base:
... @classmethod
... def mth(clz):
... print('base mth')
...
>>> class P(Base):
... pass
...
>>> super(P, P).mth
<bound method Base.mth of <class '__main__.P'>>
>>>
這就是以 super(type, obj)
、super(type, type2)
形式呼叫,傳回的會是 bound super object 的意思;相對地,super(type)
形式,因為缺少第二個引數,也就沒有物件可以綁定:
>>> super(P)
<super: <class 'P'>, NULL>
>>>
從傳回的 super
實例描述字串可以看到,NULL
代表未綁定任何物件,這就是 unbound super object 的意思,暫時也就沒有 __mro__
可以作為尋找屬性的依據,問題在於,這個 unbound super object 可以做什麼呢?呃!幾乎沒什麼可應用的場合,基本上,你可以忽略這個使用方式!
如果真要進一步探究的話,這個 unbound super object 是可以進一步指定要綁定的物件,方式是透過它的 __get__
方法,來回到實例方法的情境看看:
>>> class Base:
... def mth(self):
... print('base mth')
...
>>> class P(Base):
... pass
...
>>> s = super(P)
>>> s
<super: <class 'P'>, NULL>
>>> p = P()
>>> s.__get__(p, P)
<super: <class 'P'>, <P object>>
>>> s.__get__(p, P).mth
<bound method Base.mth of <__main__.P object at 0x00000157A04A8400>>
>>> s.__get__(p, P).mth()
base mth
>>>
在 s.__get__(p, P)
時,可以看到傳回的物件成了 bound super object,進一步地,s.__get__(p, P).mth
就可以傳回綁定方法,s.__get__(p, P).mth()
也能順利執行。
也就是說,super
建立的物件是個描述器,如果要用得比較像個描述器:
>>> class Base:
... def mth(self):
... print('base mth')
...
>>> class P(Base):
... def mth(self):
... print('P mth')
...
>>> class S(P):
... SUPER = super(P)
...
... def mth(self):
... self.SUPER.mth()
... print('S mth')
...
>>> s = S()
>>> s.mth()
base mth
S mth
>>>
簡單來說,super(type)
傳回的 super
實例,可以在後續才綁定物件,如果你想到真有什麼特別的場合,非得有這種機制才能實現,還請告訴我…XD
模擬 super(type, obj)
super
其實是個類別,以 super(type, obj)
形式呼叫的話,會建立 super
的實例,如果在第二個引數可提供的 __mro__
上找到 type
上一個類別,後續呼叫的屬性存在時,會傳回綁定方法,方法的第一個參數,會綁定為 super
第二個引數指定的物件。
那麼來試著自行實作 Super
吧!
from functools import partial
class Super:
def __init__(self, clz, obj):
if isinstance(obj, clz):
mro = type(obj).mro()
else:
raise TypeError('super(clz, obj): obj must be an instance or subtype of clz')
self.__lookup = mro[mro.index(clz) + 1:]
self.__obj = obj
def __getattr__(self, name):
for cls in self.__lookup:
if not hasattr(cls, name):
continue
attr = getattr(cls, name)
return partial(attr, self.__obj)
raise AttributeError(name)
class Base:
def mth(self):
print('base mth')
class P1(Base):
def mth(self):
Super(__class__, self).mth()
print('P1 mth')
class P2(Base):
def mth(self):
Super(__class__, self).mth()
print('P2 mth')
class S1(P1, P2):
def mth(self):
Super(__class__, self).mth()
s1 = S1()
s1.mth()