Python進階-繼承中的MRO與super

Python進階-繼承中的MRO與super

@(Python)[python, python進階]

[TOC]

寫在前面

如非特別說明堪藐,下文均基于Python3

摘要
本文講述Python繼承關系中如何通過super()調用“父類”方法减俏,super(Type, CurrentClass)返回CurrentClassMROType的下一個類的代理;以及如何設計Python類以便正確初始化。

1. 單繼承中父類方法調用

在繼承中检眯,調用父類方法是很有必要的。調用父類方法的場景有很多:

  • 比如必須調用父類的構造方法__init__才能正確初始化父類實例屬性昆淡,使得子類實例對象能夠繼承到父類實例對象的實例屬性;
  • 再如需要重寫父類方法時刽严,有時候沒有必要完全摒棄父類實現昂灵,只是在父類實現前后加一些實現避凝,最終還是要調用父類方法

單繼承是最簡單的繼承關系,多繼承過于復雜眨补,而且使用起來容易出錯管削。因此一些高級語言完全摒棄了多繼承,只支持單繼承撑螺;一些高級語言雖然支持多繼承含思,但也不推薦使用多繼承。Python也是一樣甘晤,在不能完全掌握多繼承時含潘,最好不好使用,單繼承能滿足絕大部分的需求线婚。

1.1 非綁定方式調用

綁定方法與非綁定方法的區(qū)別與聯(lián)系參見:Python基礎-類

如有以下繼承關系兩個類:

class D(object):
    def test(self):
        print('test in D')

class C(D):
    def test(self):
        print('test in C')

現在要求在子類Ctest函數中調用父類Dtest實現遏弱。我們能想到最直接的方法恐怕是直接引用類對象D的函數成員test了:

class D(object):
    def test(self):
        print('test in D')

class C(D):
    def test(self):
        print('test in C')
        D.test(self)

嘗試測試一下:

c = C()
c.test()

output:

test in C
test in D

看來非綁定的方式確實滿足了當前調用父類方法的需求。

1.2 builtin 函數 super

參考Python tutorial關于super的描述: super(\[type\[, object-or-type\]\])

Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class. The search order is same as that used by getattr() except that the type itself is skipped.

super函數返回委托類type的父類或者兄弟類方法調用的代理對象塞弊。super用來調用已經在子類中重寫了的父類方法漱逸。方法的搜索順序與getattr()函數相同,只是參數類type本身被忽略游沿。

1.3 綁定方式調用

使用綁定方式調用父類方法饰抒,自然不能顯式傳入參數當前對象(self)。現在super函數能夠返回對父類的代理诀黍,因為在單繼承中子類有且僅有一個父類袋坑,所以父類是明確的,我們完全清楚調用的父類方法是哪個:

class D(object):
    def test(self):
        print('test in D')

class C(D):
    def test(self):
        print('test in C')
        super().test() # super(C, self).test()的省略形式

2. 深入super

事實上蔗草,super函數返回的代理對象是一個bultin class super咒彤,正如它的名字所指,類super代理了子類的父類咒精。在單繼承關系中镶柱,super代理的類很容易找到嗎,就是子類的唯一父類模叙;但是在多繼承關系中歇拆,super除了能代理子類的父類外,還有可能代理子類的兄弟類范咨。

2.1 復雜的多繼承

在多繼承關系中故觅,繼承關系可能會相當復雜。

class D(object):
    
    def test(self):
        print('test in D')

class C(D):
    
    def test(self):
        print('test in C')

class B(D):
    
    def test(self):
        print('test in B')

class A(B, C):
    pass

A繼承層次結構如下:

  object
    |
    D
   / \
  B   C
   \ /
    A

A的繼承關系中存在菱形結構渠啊,即可以通過多條路徑從類A到達某個父類输吏,這里是D

如果現在要求在類A中調用“父類”的test方法替蛉,需要一種對test方法的搜索解析順序贯溅,來決定到底是調用B,C或Dtest方法拄氯。

2.2 方法解析順序(MRO)

上面提出的對test的方法的搜索順序,就是方法解析順序了它浅。

深度優(yōu)先
Python舊式類中译柏,方法解析順序是深度優(yōu)先,多個父類從左到右姐霍。
廣度優(yōu)先
Python新式類中鄙麦,方法解析順序是廣度優(yōu)先,多個父類從左到右镊折。

所以上面的解析順序是:A -> B -> C -> D -> object胯府。

Python中,類的__mro__屬性展示了方法搜索順序腌乡,可以調用mro()方法或者直接引用__mro__得到搜索順序:

print(A.mro())
print(A.__mro__)

output:

[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>]
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)

所以

a = A()
a.test() # output: test in B

變化的MRO
即使是同一個類盟劫,在不同的MRO中位置的前后關系都是不同的。如以下類:

class D(object):
    
    def test(self):
        print('test in D')

class C(D):
    
    def test(self):
        print('test in C')

class B(D):
    
    def test(self):
        print('test in B')

B的繼承層次結構為:

  object
    |
    D
   / \
  C   B

B的MRO:B -> D -> object
對比類A的MRO:A -> B -> C -> D -> object
同樣的類B与纽,在兩個不同的MRO中位置關系也是不同的侣签。可以說急迂,在已有的繼承關系中加入新的子類影所,會在MRO中引入新的類,并且改變解析順序僚碎。

那么可以想象猴娩,同樣在類B的test中通過super調用父類方法,在不同的MRO中實際調用的方法是不同的勺阐。

如下:

class D(object):
    
    def test(self):
        print('test in D')

class C(D):
    
    def test(self):
        print('test in C')
        super().test()

class B(D):
    
    def test(self):
        print('test in B')
        super().test()

class A(B, C):
    pass

b = B()
b.test()
print('==========')
a = A()
a.test()

output:

test in B
test in D
==========
test in B
test in C
test in D

因為在原有的類關系中加入BC的子類A卷中,使得在Btest方法中調用supertest方法發(fā)生了改變,原來調用的是其父類Dtest方法渊抽,現在調用的是其兄弟類Ctest方法蟆豫。
從這里可以看出super不總是代理子類的父類,還有可能代理其兄弟類懒闷。

因此在設計多繼承關系的類體系時十减,要特別注意這一點。

2.3 再看super方法

方法super([type[, object-or-type]])愤估,返回的是對type的父類或兄弟類的代理帮辟。

  • 如果第二個參數省略,返回的super對象是未綁定到確定的MRO上的;
  • 如果第二個參數是對象玩焰,那么isinstance(obj, type)必須為True由驹;
  • 如果第二個參數是類型,那么issubclass(type2, type)必須為True昔园,即第二個參數類型是第一個參數類型的子類荔棉。

super函數的第二個參數存在時闹炉,其實現大概如以下:

def super(cls, inst):
    mro = inst.__class__.mro() # Always the most derived class
    return mro[mro.index(cls) + 1]

很明顯,super返回在第二個參數對應類的MRO列表中润樱,第一個參數type的下一個類的代理。因此羡棵,要求第一個參數type存在于第二個參數類的MRO是必要的壹若,只有第一個參數類是第二個參數所對應類的父類,才能保證皂冰。

super()
super函數是要求有參數的店展,不存在無參的super函數。在類定義中以super()方式調用秃流,是一種省略寫法赂蕴,由解釋器填充必要參數。填充的第一個參數是當前類舶胀,第二個參數是self

super() => super(current_class, self)

所以概说,super()這種寫法注定只能在類定義中使用。

現在再來看上面的繼承關系:

class D(object):
    def test(self):
        print('test in D')

class C(D):
    def test(self):
        print('test in C')
        # super().test() # 與下面的寫法等價
        super(C, self).test() # 返回self對應類的MRO中嚣伐,類C的下一個類的代理

class B(D):
    def test(self):
        print('test in B')
        # super().test() # 與下面的寫法等價
        super(B, self).test() # 返回self對應類的MRO中糖赔,類B的下一個類的代理

class A(B, C):
    pass

因此:

b = B()
b.test() # 基于類B的MRO(B->D->object),類B中的super()代理D
print('==========')
a = A()
a.test() # 基于類A的MRO(A->B->C->D->object)轩端,類B中的super()代理C

以上就是在繼承關系中引入新類放典,改變方法解析順序的實例。

super([type[, object-or-type]])的第二個參數基茵,對象和類還有一點區(qū)別:使用對象返回的是代理使用綁定方法奋构,使用類返回的代理使用非綁定方法。
如:

b = B()
super(B, b).test()
super(B, B).test(b)

這兩種方式得到的結果是相同的拱层,區(qū)別在于非綁定調用與綁定調用弥臼。

3. 最佳實踐

3.1 不可預測的調用

普通的函數或者方法調用中,調用者肯定事先知道被調用者所需的參數舱呻,然后可以輕松的組織參數調用醋火。但是在多繼承關系中,情況有些尷尬箱吕,使用super代理調用方法芥驳,編寫類的作者并不知道最終會調用哪個類的方法,這個類都可能尚未存在茬高。

如現在一作者編寫了以下類:

class D(object):
    def test(self):
        print('test in D')
        
class B(D):
    def test(self):
        print('test in B')
        super().test()

在定義類D時兆旬,作者完全不可能知道test方法中的super().test()最終會調用到哪個類。
因為如果后來有人在這個類體系的基礎上怎栽,引入了如下類:

class C(D):
    def test(self):
        print('test in C')
        super().test()
        
class A(B, C):
    pass

a = A()
a.test()

此時會發(fā)現類Btest方法中super().test()調用了非原作者編寫的類的方法丽猬。
這里test方法的參數都是確定的宿饱,但是在實際生產中,可能各個類的test方法都是不同的脚祟,如果新引入的類C需要不同的參數:

class C(D):
    def test(self, param_c):
        print('test in C, param is', param_c)
        super().test()
        
class A(B, C):
    pass

a = A()
a.test()

B的調用方式調用類Ctest方法肯定會失敗谬以,因為沒有提供任何參數。類C的作者是不可能去修改類B的實現由桌。那么为黎,如何適應這種參數變換的需求,是在設計Python類中需要考慮的問題行您。

3.2 實踐建議

事實上铭乾,這種參數的變換在構造方法上能體現得淋漓盡致,如果子類沒有正確初始化父類娃循,那么子類甚至不能從父類繼承到需要的實例屬性炕檩。

所以,Python的類必須設計友好捌斧,才能拓展笛质,有以下三條指導原則:

  1. 通過super()調用的方法必須存在;
  2. 調用者和被調用者參數必須匹配骤星;
  3. 所有對父類方法的調用都必須使用super()
3.3 參數匹配

super()代理的類是不可預測的经瓷,需要匹配調用者和可能未知的調用者的參數。

固定參數
一種方法是使用位置參數固定函數簽名洞难。就像以上使用的test()一樣舆吮,其簽名是固定的,只要要傳遞固定的參數队贱,總是不會出錯色冀。

關鍵字參數
每個類的構造方法可能需要不同的參數,這時固定參數滿足不了這種需求了柱嫌。幸好锋恬,Python中的關鍵字參數可以滿足不定參數的需求。設計函數參數時编丘,參數由關鍵字參數和關鍵字參數字典組成与学,在調用鏈中,每一個函數獲取其所需的關鍵字參數嘉抓,保留不需要的參數到**kwargs中索守,傳遞到調用鏈的下一個函數,最終**kwargs為空時抑片,調用調用鏈中的最后一個函數卵佛。

示例:

class Shape(object):
    def __init__(self, shapename, **kwargs):
        self.shapename = shapename
        super().__init__(**kwargs)

class ColoredShape(Shape):
    def __init__(self, color, **kwargs):
        self.color = color
        super().__init__(**kwargs)

cs = ColoredShape(color='red', shapename='circle')

參數的剝落步驟為:

  • 使用cs = ColoredShape(color='red', shapename='circle')初始化ColoredShape
  • ColoredShape__init__方法獲取其需要的關鍵字參數color,此時的kwargs{shapename:'circle'};
  • 調用調用鏈中Shape__init__方法截汪,該方法獲取所需關鍵字參數shapename疾牲,此時kwargs{};
  • 最后調用調用鏈末端objet.__init__,此時因為kwargs已經為空衙解。

初始化子類傳遞的關鍵字參數尤為重要阳柔,如果少傳或多傳,都會導致初始化不成功蚓峦。只有MRO中每個類的方法都是用super()來調用“父類”方法時盔沫,才能保證super()調用鏈不會斷掉。

3.4 保證方法存在

上面的例子中枫匾,由于頂層父類object總是存在__init__方法,在任何MRO鏈中也總是最后一個拟淮,因此任意的super().__init__調用總能保證是object.__init__結束干茉。

但是其他自定義的方法得不到這樣的保證。這時需要手動創(chuàng)建類似object的頂層父類:

class Root:
    def draw(self):
        # the delegation chain stops here
        assert not hasattr(super(), 'draw')

class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting shape to:', self.shapename)
        super().draw()

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting color to:', self.color)
        super().draw()

cs = ColoredShape(color='blue', shapename='square')
cs.draw()

如果有新的類要加入到這個MRO體系很泊,新的子類也要繼承Root角虫,這樣,所有的對draw()的調用都會經過Root委造,而不會到達沒有draw方法的object了戳鹅。這種對于子類的擴展要求,應當詳細注明在文檔中昏兆,便于使用者閱讀枫虏。這種限制與Python所有異常都必須繼承自BaseException一樣。

3.5 組合不友好的類

對于那些不友好的類:

class Moveable:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)

如果希望使用它的功能爬虱,直接將其加入到我們友好的繼承體系中隶债,會破壞原有類的友好性。
除了通過繼承獲得第三方功能外跑筝,還有一種稱之為組合的方式死讹,即把第三方類作為組件的方式揉入類中,使得類具有第三方的功能:

class MoveableAdapter(Root):
    def __init__(self, x, y, **kwds):
        self.movable = Moveable(x, y)
        super().__init__(**kwds)
    def draw(self):
        self.movable.draw()
        super().draw()

Moveable被作為組件整合到適配類MoveableAdapter中曲梗,適配類擁有了Moveable的功能赞警,而且是友好實現的。完全可以通過繼承適配類的方式虏两,將Moveable的功能加入到友好的繼承體系中:

class MovableColoredShape(ColoredShape, MoveableAdapter):
    pass

MovableColoredShape(color='red', shapename='triangle',
                    x=10, y=20).draw()

參考

Python’s super() considered super!
Python tutorial#super

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末愧旦,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子碘举,更是在濱河造成了極大的恐慌忘瓦,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異耕皮,居然都是意外死亡境蜕,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門凌停,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粱年,“玉大人,你說我怎么就攤上這事罚拟√ㄊ” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵赐俗,是天一觀的道長拉队。 經常有香客問我,道長阻逮,這世上最難降的妖魔是什么粱快? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮叔扼,結果婚禮上事哭,老公的妹妹穿的比我還像新娘。我一直安慰自己瓜富,他們只是感情好鳍咱,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著与柑,像睡著了一般谤辜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仅胞,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天每辟,我揣著相機與錄音,去河邊找鬼干旧。 笑死渠欺,一個胖子當著我的面吹牛,可吹牛的內容都是我干的椎眯。 我是一名探鬼主播挠将,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼编整!你這毒婦竟也來了舔稀?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掌测,失蹤者是張志新(化名)和其女友劉穎内贮,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡夜郁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年什燕,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竞端。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡屎即,死狀恐怖,靈堂內的尸體忽然破棺而出事富,到底是詐尸還是另有隱情技俐,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布统台,位于F島的核電站雕擂,受9級特大地震影響,放射性物質發(fā)生泄漏贱勃。R本人自食惡果不足惜捂刺,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望募寨。 院中可真熱鬧,春花似錦森缠、人聲如沸拔鹰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽列肢。三九已至,卻和暖如春宾茂,著一層夾襖步出監(jiān)牢的瞬間瓷马,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工跨晴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留欧聘,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓端盆,卻偏偏與公主長得像怀骤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子焕妙,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理蒋伦,服務發(fā)現,斷路器焚鹊,智...
    卡卡羅2017閱讀 134,707評論 18 139
  • 兩本不錯的書: 《Python參考手冊》:對Python各個標準模塊痕届,特性介紹的比較詳細。 《Python核心編程...
    靜熙老師哈哈哈閱讀 3,363評論 0 80
  • [TOC] 面向對象 繼承與派生 繼承 什么是繼承?繼承是一種創(chuàng)建新的類的方式 在python中研叫,新建的類可以繼承...
    派大星的喜悲沒人看見閱讀 305評論 0 0
  • @(python)[筆記] 目錄 前言 在python中蓝撇,一切皆對象面向對象的程序設計的核心就是對象果复;面向對象的程...
    CaiGuangyin閱讀 594評論 0 5
  • 轉至元數據結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,726評論 0 9