Python的繼承以及呼叫父類成員:super用法
阿新 • • 發佈:2018-11-26
python子類呼叫父類成員有2種方法,分別是普通方法和super方法 假設Base是基類 class Base(object): def __init__(self): print “Base init” 則普通方法如下 class Leaf(Base): def __init__(self): Base.__init__(self) print “Leaf init” super方法如下 class Leaf(Base): def __init__(self): super(Leaf, self).__init__() print “Leaf init” 在上面的簡單場景下,兩種方法的效果一致: >>> leaf = Leaf() Base init Leaf init 2. 鑽石繼承遇到的難題 當我們來到鑽石繼承場景時,我們就遇到了一個難題: 如果我們還是使用普通方法呼叫父類成員,程式碼如下: class Base(object): def __init__(self): print “Base init” class Medium1(Base): def __init__(self): Base.__init__(self) print “Medium1 init” class Medium2(Base): def __init__(self): Base.__init__(self) print “Medium2 init” class Leaf(Medium1, Medium2): def __init__(self): Medium1.__init__(self) Medium2.__init__(self) print “Leaf init” 當我們生成Leaf物件時,結果如下: >>> leaf = Leaf() Base init Medium1 init Base init Medium2 init Leaf init 可以看到Base被初始化了 兩次 !這是由於Medium1和Medium2各自呼叫了Base的初始化函式導致的。 3. 各語言的解決方法 鑽石繼承中,父類被多次初始化是個非常難纏的問題,我們來看看其他各個語言是如何解決這個問題的: 3.1. C++ C++使用虛擬繼承來解決鑽石繼承問題。 Medium1和Medium2虛擬繼承Base。當生成Leaf物件時,Medium1和Medium2並不會自動呼叫虛擬基類Base的建構函式,而需要由Leaf的建構函式顯式呼叫Base的建構函式。 3.2. Java Java禁止使用多繼承。 Java使用單繼承+介面實現的方式來替代多繼承,避免了鑽石繼承產生的各種問題。 3.3. Ruby Ruby禁止使用多繼承。 Ruby和Java一樣只支援單繼承,但它對多繼承的替代方式和Java不同。Ruby使用Mixin的方式來替代,在當前類中mixin入其他模組,來做到程式碼的組裝效果。 3.4. Python Python和C++一樣,支援多繼承的語法。但Python的解決思路和C++完全不一樣,Python是的用就是super 我們把第2章的鑽石繼承用super重寫一下,看一下輸出結果 class Base(object): def __init__(self): print “Base init” class Medium1(Base): def __init__(self): super(Medium1, self).__init__() print “Medium1 init” class Medium2(Base): def __init__(self): super(Medium2, self).__init__() print “Medium2 init” class Leaf(Medium1, Medium2): def __init__(self): super(Leaf, self).__init__() print “Leaf init” 我們生成Leaf物件: >>> leaf = Leaf() Base init Medium2 init Medium1 init Leaf init 可以看到整個初始化過程符合我們的預期,Base只被初始化了1次。而且重要的是,相比原來的普通寫法,super方法並沒有寫額外的程式碼,也沒有引入額外的概念 4. super的核心:mro 要理解super的原理,就要先了解mro。mro是method resolution order的縮寫,表示了類繼承體系中的成員解析順序。 在python中,每個類都有一個mro的類方法。我們來看一下鑽石繼承中,Leaf類的mro是什麼樣子的: >>> Leaf.mro() [Leaf, Medium1, Medium2, Base] 可以看到mro方法返回的是一個祖先類的列表。Leaf的每個祖先都在其中出現一次,這也是super在父類中查詢成員的順序。 通過mro,python巧妙地將多繼承的圖結構,轉變為list的順序結構。super在繼承體系中向上的查詢過程,變成了在mro中向右的線性查詢過程,任何類都只會被處理一次。 通過這個方法,python解決了多繼承中的2大難題: 1. 查詢順序問題。從Leaf的mro順序可以看出,如果Leaf類通過super來訪問父類成員,那麼Medium1的成員會在Medium2之前被首先訪問到。如果Medium1和Medium2都沒有找到,最後再到Base中查詢。 2. 鑽石繼承的多次初始化問題。在mro的list中,Base類只出現了一次。事實上任何類都只會在mro list中出現一次。這就確保了super向上呼叫的過程中,任何祖先類的方法都只會被執行一次。 至於mro的生成演算法,可以參考這篇wiki:https://en.wikipedia.org/wiki/C3_linearization 5. super的具體用法 我們首先來看一下python中的super文件 >>> help(super) Help on class super in module __builtin__: class super(object) | super(type, obj) -> bound super object; requires isinstance(obj, type) | super(type) -> unbound super object | super(type, type2) -> bound super object; requires issubclass(type2, type) 光從字面來看,這可以算是python中最語焉不詳的幫助文件之一了。甚至裡面還有一些術語誤用。那super究竟應該怎麼用呢,我們重點來看super中的第1和第3種用法 5.1. super(type, obj) 當我們在Leaf的__init__中寫這樣的super時: class Leaf(Medium1, Medium2): def __init__(self): super(Leaf, self).__init__() print “Leaf init” super(Leaf, self).__init__()的意思是說: 獲取self所屬類的mro, 也就是[Leaf, Medium1, Medium2, Base] 從mro中Leaf右邊的一個類開始,依次尋找__init__函式。這裡是從Medium1開始尋找 一旦找到,就把找到的__init__函式繫結到self物件,並返回 從這個執行流程可以看到,如果我們不想呼叫Medium1的__init__,而想要呼叫Medium2的__init__,那麼super應該寫成:super(Medium1, self)__init__() 5.2. super(type, type2) 當我們在Leaf中寫類方法的super時: class Leaf(Medium1, Medium2): def __new__(cls): obj = super(Leaf, cls).__new__(cls) print “Leaf new” return obj super(Leaf, cls).__new__(cls)的意思是說: 獲取cls這個類的mro,這裡也是[Leaf, Medium1, Medium2, Base] 從mro中Leaf右邊的一個類開始,依次尋找__new__函式 一旦找到,就返回“ 非繫結 ”的__new__函式 由於返回的是非繫結的函式物件,因此呼叫時不能省略函式的第一個引數。這也是這裡呼叫__new__時,需要傳入引數cls的原因 同樣的,如果我們想從某個mro的某個位置開始查詢,只需要修改super的第一個引數就行 6. 小結 至此,我們講解了和super相關的用法及原理,小結一下我們講過的內容有: python呼叫父類成員共有2種方法:普通方法,super方法 在鑽石繼承中,普通方法會遇到Base類兩次初始化的問題 簡述了其他語言對這個問題的解決方法,並用例項展示了python使用super可以解決此問題 在講super具體用法前,先講了super的核心:mro的知識和原理 講解了super兩種主要的用法及原理