Python學習之路31 繼承的利弊

2021-09-11 13:54:16 字數 3989 閱讀 2812

《流暢的python》筆記

本篇是「物件導向慣用方法」的第五篇,我們將繼續討論繼承,重點說明兩個方面:繼承內建型別時的問題以及多重繼承。概念比較多,較為枯燥。

內建型別(c語言編寫)的方法通常會忽略使用者重寫的方法,這種行為體現在兩方面:

dict__getitem__方法為例,即使這個方法被子類重寫了,內建型別的get()方法也不一定呼叫重寫的版本:

# **1.1

>>>

class

mydict

(dict):

...

def__getitem__

(self, key):

...

return

"test"

# 不管要獲取誰,都返回"test"

...

>>> child = mydict()

>>> child

# 正常

>>> child["one"]

'test'

# 此時也是正常的

>>> child.get("one")

1# 這裡就不正常了,按理說應該返回"test"

>>> b = {}

>>> b.update(child)

>>> b # 並沒有呼叫child的__getitem__方法

複製**

這是在cpython中的情況,這些行為其實違背了物件導向程式設計的乙個基本原則,即應該始終從例項所屬的類開始搜尋方法,即使在超類實現的類中呼叫也應該如此。但實際是可能直接呼叫基類的方法,而不先搜尋子類。這種設定並不能說是錯誤的,這只是一種取捨,畢竟這也是cpython中的內建型別執行得快的原因之一,但這種方式就給我們出了難題。這種問題的解決方法有兩個:

強調:本節所述問題只發生在c語言實現的內建型別內部的方法委託上,而且只影響直接繼承內建型別的自定義類。如果子類繼承自純python編寫的類,則不會有此問題。

任何實現多重繼承的語言都要處理潛在的命名衝突,這種衝突由不相關的超類實現同名方法引起。這種衝突稱為」菱形衝突「。

下面是我們要實現的類的uml圖:

紅線表示超類的呼叫順序,以下是它的實現:

# **2.1

class a:

def ping(self):

print("ping in a:", self)

class b(a):

def pong(self):

print("pong in b:", self)

class c(a):

def pong(self):

print("pong in c:", self)

class d(b, c):

def ping(self):

super().ping()

print("ping in d:", self)

def pingpong(self):

self.ping()

super().ping()

self.pong()

super().pong()

c.pong(self) # 在定義時呼叫特定父類的寫法,顯示傳入self引數

# 下面是它在控制台中的呼叫情況

>>> from diamond import *

>>> d = d()

>>> d.pong()

pong in b: >>> d.pingpong()

ping in a: # self.ping()

ping in d: ping in a: # super().ping()

pong in b: # self.pong()

pong in b: # super().pong()

pong in c: # c.pong(self)

>>> c.pong(d) # 在執行時呼叫特定父類的寫法,顯示傳入例項引數

(, , ,

, )複製**

類都有乙個名為__mro__的屬性,它的值是乙個元組,按一定順序列舉超類,這個順序由c3演算法計算。

方法解析順序不僅考慮繼承圖,還考慮子類宣告中列出超類的順序。例如,如果d類的宣告改為class d(c, b),那麼d則會先搜尋c,再搜尋b

若想把方法呼叫委託給超類,推薦的做法是使用內建的super()函式;同時,還請注意上述呼叫特定超類的語法。然而,使用super()是最安全的,也不易過時。呼叫框架或不受自己控制的類層次結構中的方法時,尤其應該使用super()

繼承有很多用途,而多重繼承增加了可選方案和複雜度。使用多重繼承容易得出令人費解和脆弱的設計。以下是8條避免產生混亂類圖的建議:

把介面繼承和實現繼承區分開

在使用多重繼承時,一定要明白自己為什麼要建立子類:

其實這倆經常同時出現,不過只要有可能,一定要明確這麼做的意圖。通過繼承重用**是實現細節,通常可以換成用組合和委託的模式,而介面繼承則是框架的支柱。

使用抽象基類顯示表示介面

如果類的作用是定義介面,應該將其明確定義為抽象基類。

通過「混入類」實現**重用

如果乙個類的作用是為多個不相關的子類提供方法實現,從而實現重用,但不體現「is-a」關係,則應該把那個類明確定義為混入類(mixin class)。從概念上講,混入不定義新型別,只是打包方法,便於重用。混入類絕對不能例項化,而且具體類不能只繼承混入類。混入類應該提供某方面的特定行為,只實現少量關係非常緊密的方法。

在名稱中明確指明混入

由於python沒有把類明確宣告為混入的正式方式,實際的做法是在類名後面加入mixin字尾。python的gui庫tkinter沒有採用這種方法,這也是它的類圖十分混亂的原因之一,而django則採用了這種方式。

抽象基類可以作為混入類,但混入類不能作為抽象基類

抽象基類可以實現具體方法,因此可以作為混入類使用。但抽象基類能定義資料型別,混入類則做不到。此外,抽象基類可以作為其他類的唯一基類,混入類則決不能作為唯一的基類,除非這個混入類繼承了另乙個更具體的混入(這種做法非常少見)。

但值得注意的是,抽象基類中的具體方法只是一種便利措施,因為它只能呼叫抽象基類及其超類中定義了的方法,那麼使用者自行呼叫這些方法也可以實現同樣的功能,所以,抽象基類也並不常作為混入類。

不要從多個具體類繼承

應該盡量保證具體類沒有或者最多只有乙個具體超類。也就是說,具體類的超類中除了這乙個具體超類外,其餘的都應該是抽象基類或混入類。

為使用者提供聚合類

如果抽象基類或混入類的組合對客戶**非常有用,那就提供乙個類,使用易於理解的方式把它們結合起來,這種類被稱為聚合類。比如tkinter.widget類,它的定義如下:

# **2.2

class

widget

(basewidget, pack, place, grid):

# 省略掉了文件注釋

pass

複製**

它的定義體是空的,但通過這乙個類,提供了四個超類的全部方法。

優先使用物件組合,而不是類繼承

優先使用組合能讓設計更靈活。即便是單繼承,這個原則也能提公升靈活性,因為繼承是一種緊耦合,而且較高的繼承樹容易倒。組合和委託還可以代替混入類,把行為提供給不同的類,但它不能取代介面繼承,因為介面繼承定義的是類層次結構。

Python學習之路 類繼承

如果兩個類具有同名的屬性和方法的時候就可以使用繼承,例如b類繼承a類,那麼在b類中就有類a中的屬性以及方法。被繼承的類叫做父類,繼承的而得類叫做子類。繼承是物件導向程式設計的第二個特性。一般來說,父類是一些公有的屬性和方法,因此類的繼承能夠減少 的冗餘,提公升 的可讀性,提高開發效率。幾乎在所有物件...

python之路 鑽石繼承

繼承順序 class a object deftest self print from a class b a deftest self print from b class c a deftest self print from c class d b deftest self print fro...

C 學習之路 繼承機制

1 繼承 c 的訪問控制方式有三種 public 公有繼承 protected 保護繼承 和private 私有繼承 c 派生類繼承了基類的所有資料型別和除了建構函式和析構函式的所有成員函式。基類的public 基類的protected 基類的private 公有繼承 public protecte...