Python 類別繼承介紹,以及實作 Python 類別繼承,多重繼承,方法覆寫

閱讀 15:16·字數 4585·更新 
Youtube 頻道
訂閱 133

先決條件

閱讀本節的先決條件是對 Python 類別以及類別的多型有所掌握,你可以檢視Python 類別介紹,以及定義和使用 Python 類別程式設計教學類別的多型,方法覆寫,方法多載,方法遮蔽介紹來了解相關資訊。

繼承 Python 類別

Python 支援類別的繼承,你可以定義一個從基底類別衍生的類別,其語法的基本形式如下。

class <classname>(<baseclass>):
    <block>

classname 部分

classname為衍生類別的名稱,他需要符合 Python 的識別碼規格,不能使用 Python 關鍵字或保留關鍵字。

baseclass 部分

baseclass是一個傳回基底類別的運算式,其傳回的基底類別應是可存取的,這與使用其他 Python 模組特性(比如,變數,函式)類似。

block 部分

block為衍生類別的主體程式碼,需要使用某種空白字元進行縮排,以表示其歸屬於衍生類別。

下面的類別GoodMan繼承自類別Person

inherit.py
# 一個關於人的類別
class Person:
	title = '該如何稱呼哪?'

def __init__(self, name, age): # 定義類別的執行個體變數 name,age self.name = name self.age = age

# 表示好人的類別,從 Person 繼承 class GoodMan(Person): pass

判斷 Python 物件的型別是否為某個類別或其衍生類別

使用函式isinstance,你可以判斷一個 Python 物件(執行個體)的型別,是否為某個 Python 類別(包括抽象類別)或其衍生類別(子類別),或者是否為一組 Python 類別中的某個類別(包括抽象類別)或其衍生類別(子類別),傳回True表示物件的型別是某個類別或其衍生類別,傳回False表示不是。

isinstance(object, classinfo, /)

object 參數

object參數為需要判斷型別的 Python 物件。

classinfo 參數

classinfo參數為某個 Python 類別,包含 Python 類別的元組,或 Python 類別的聯集型別物件(需要 Python 3.10 或更高版本),聯集型別(UnionType)物件可通過運算子|組合多個 Python 類別獲得。

inherit.py
# …
# 建立 GoodMan 的執行個體
goodman = GoodMan('好心人', 40)

# 判斷執行個體 goodman 的型別是否為 Person 或 int print(isinstance(goodman, (Person, int))) # 判斷執行個體 goodman 的型別是否為 int,float,str 中的一個 print(isinstance(goodman, int | float | str))
True
False

判斷 Python 類別是否為某個類別的衍生類別

使用函式issubclass,你可以判斷一個 Python 類別,是否為某個 Python 類別(包括抽象類別)的衍生類別(子類別),或者是否為一組 Python 類別中的某個類別(包括抽象類別)的衍生類別(子類別),傳回True表示類別是衍生類別,傳回False表示不是。需要指出的是,issubclass會將一個 Python 類別視為其自身的衍生類別。

issubclass(class, classinfo, /)

class 參數

class參數為需要判斷是否為衍生類別的 Python 類別。

classinfo 參數

classinfo參數為某個 Python 類別,包含 Python 類別的元組,或 Python 類別的聯集型別物件(需要 Python 3.10 或更高版本),聯集型別(UnionType)物件可通過運算子|組合多個 Python 類別獲得。

inherit.py
# …
# 判斷類別 GoodMan 是否為 object 的子類別
print(issubclass(GoodMan, object))
# 判斷類別 GoodMan 是否為 str 或 Person 的子類別
print(issubclass(GoodMan, str | Person))
# 判斷類別 GoodMan 是否為 GoodMan 的子類別
print(issubclass(GoodMan, GoodMan))
True
True
True

搜尋 Python 繼承鏈中的特性

如果物件對應的 Python 類別中不存在想要存取的特性,可以是指執行個體特性或類別特性,那麽 Python 將在繼承鏈的一個或多個基底類別中搜尋,直至此特性被找到,或整個繼承鏈被搜尋完畢。對於讀取操作(包括呼叫方法),未搜尋到特性會引發例外狀況,對於寫入操作,未搜尋到特性會導致新的特性被加入 Python 物件。

繼承鏈中 Python 類別和其基底類別之間的搜尋順序,需要通過 Python 的方法解析順序(Method Resolution Order,MRO)來確定,Python 物件實際對應的類別一般處於該順序的首位。

當然,通過 Python 類別存取類別特性,與通過 Python 物件存取執行個體特性或類別特性具有類似的搜尋機製,只不過其搜尋不涵蓋 Python 類別的執行個體特性。

無法通過變數型別應用 Python 類別的多型

雖然可以通過:進一步說明 Python 變數,但你無法真正的為 Python 變數宣告一種型別,因此,當通過變數存取 Python 物件的特性時,其尋找的起點總是該物件(執行個體)的實際型別,而不像某些語言一樣以變數的型別為起點,這種情況同樣適用於 Python 參數,傳回值(傳回值可以使用->進行說明)。

下面的類別Tree繼承自類別Planttree.show()會呼叫定義在Tree類別中的show方法,該方法將存取執行個體變數name,雖然Tree沒有直接定義,但name可以在Plant中搜尋到,這與通過Tree.variety存取類別變數variety的情況類似。

寫入操作tree.age=100將為tree新增執行個體變數age,因為在此之前age並不存在。讀取操作Tree.name會導致例外狀況AttributeError,因為類別變數name不存在。

inherit_search.py
# 一個關於植物的類別
class Plant:
	# 類別變數 variety
	variety = '未知種類'
	def __init__(self, name):
		# 執行個體變數 name
		self.name = name

# 繼承自 Plant 類別的類別 class Tree(Plant): # 顯示資訊的方法 def show(self): print(self.name)
tree = Tree('大樹') tree.show() print(f'Tree 是什麽種類?{Tree.variety}')
# age 並不存在,寫入操作等於為 tree 增加執行個體變數 tree.age = 100 print(tree.age) # ERROR 類別變數 name 並不存在,讀取操作將導致例外狀況 print(Tree.name)
大樹
Tree 是什麽種類?未知種類
100

AttributeError: type object 'Tree' has no attribute 'name'

覆寫 Python 類別的方法

基於上面描述的繼承鏈搜尋方式,你可以將 Python 類別中定義的所有方法都視為虛擬方法(Virtual Method),這表示他們可以被覆寫(Override),如果存取層級允許的話。與模組中定義的 Python 函式一樣,Python 類別的方法簽章預設僅包含名稱資訊,因此,衍生類別中定義的方法可以覆寫基底類別的同名方法,而不必在意參數的情況。

事實上,覆寫可針對 Python 類別的任意特性,比如變數或屬性,而不是僅限於方法。

無法直接對 Python 類別的方法進行多載

如上所述,Python 的方法簽章僅包含方法名稱,這導致你無法直接對 Python 類別的方法進行多載,如果希望 Python 類別擁有多個名稱相同但參數不同的方法,那麽需要采用其他的方式,比如,通過functools模組的singledispatchmethod類別。

下面的Hero類別對Unit類別中的attack方法進行了兩次覆寫,但只有第二次覆寫是有效的,第一次覆寫將被第二次覆寫覆蓋。嘗試通過運算式Hero().attack()呼叫Unit類別的attack方法是不可行的,因為只能定位到Hero中的attack

inherit_override.py
# 一個遊戲單位
class Unit:
	# 用於發起攻擊的方法
	def attack(self):
		print('Unit 發起攻擊')

class Hero(Unit): # 該方法將被之後定義的 attack 覆蓋 def attack(self, times): while times > 0: print('Hero 發起攻擊') times -= 1
# 將覆寫 Unit 的 attack 方法,並覆蓋之前定義的 attack def attack(self, times): print(f'Hero 將發起 {times} 次攻擊')
Unit().attack() Hero().attack(5) # ERROR 無法呼叫 Unit 類別定義的 attack 方法 Hero().attack()
Unit 發起攻擊
Hero 將發起 5 次攻擊

TypeError: Hero.attack() missing 1 required positional argument: 'times'

多重繼承 Python 類別

Python 允許類別的多重繼承,你可以為一個 Python 類別指定多個基底類別,只需要在定義類別的語法中,讓baseclass部分包含使用,分隔的多個基底類別即可。

Python 多重繼承中的語意模糊問題

在物件導向程式設計中,多重繼承帶來的最大問題就是語意模糊,Python 的方法解析順序可有效緩解該問題的發生,因為他嘗試將搜尋路徑線性化,以避免菱形路徑(語意模糊問題)的出現,每一個 Python 類別僅被搜尋一次,搜尋的結果中不會重複包含同一個 Python 類別的同一個特性。

至於多個 Python 基底類別的搜尋順序,由他們在baseclass部分的書寫順序決定,最先出現的基底類別優先順序最高,在方法解析順序中的位置靠前,之後出現的基底類別優先順序依次降低,在方法解析順序中的位置靠後。

在下面的繼承關系中,C.name不會產生語意模糊問題,雖然B1B2均繼承自類別A,但在方法解析順序中,類別A只會被搜尋一次,而不是兩次,因此類變數name不是模棱兩可的。

陳述式c.show()將呼叫B2show方法,雖然B1書寫在B2之前,B1擁有更高的優先順序,但類別A的優先順序並不在B1B2之間,而是排在B2之後。

inherit_multiple.py
class A:
	name = 'A'
	def show(self):
		print('呼叫 A 的 show 方法')

class B1(A): pass
class B2(A): def show(self): print('呼叫 B2 的 show 方法')
class C(B1, B2): pass
# 不會產生語意模糊問題,因為類別 A 僅被搜尋一次 print(C.name) c = C() # 呼叫 B2 的 show 方法,而不是 A 的 show 方法 c.show()
A
呼叫 B2 的 show 方法

呼叫 Python 基底類別中的方法

如果某個 Python 類別覆寫了基底類別中的方法,並且希望繼續使用基底類別所實作的功能,那麽可以在覆寫的方法中,使用super類別來呼叫基底類別的方法,該類別實作了一種存取的代理功能,你可以像通過 Python 執行個體或類別存取特性一樣,通過super類別的執行個體(物件)來存取特性,只不過super類別需要指定特性的搜尋起點,以實作對特定基底類別的存取。

super()
super(type, object_or_type=None, /)

type 參數

type參數是一個 Python 類別,在方法解析順序中,位於該 Python 類別之後的第一個類別將成為特性的搜尋起點。

object_or_type 參數

object_or_type參數為需要搜尋特性的 Python 執行個體或類別,他們應該是type參數所表示的 Python 類別或其衍生類別,或者是type參數所表示的 Python 類別或其衍生類別的執行個體。如果忽略該參數,那麽得到的super物件將處於未繫結狀態,他可能無法完成你的預期目標。

對於不帶有參數的super類別的建構子,僅能在 Python 類別的執行個體方法或類別方法中使用,他的效果等同於書寫super(type,self)super(type,cls),其中type為目前類別,self為執行個體方法的self參數,cls為類別方法的cls參數。

對於帶有參數的super類別的建構子,可以在 Python 類別的方法或其他位置使用,如果是類別的靜態方法,那麽需要采用形式為super(type,class)的運算式,其中type為用於指示搜尋起點的 Python 類別(方法解析順序中,排在type對應的類別之後的第一個類別將作為搜尋起點),class為目前類別。由於指定了特性的搜尋起點,因此,該建構子可實作更為細致的基底類別存取。

當然,不僅是基底類別中的方法,通過super類別存取其他特性也是可行的,比如變數,屬性。

下面的程式碼,演示了如何通過super呼叫基底類別的執行個體方法,類別方法和靜態方法。其中,陳述式super(Dog,self).run()使BigDog繞過了基底類別Dog中的run方法,陳述式super(BigDog,dog).run()則呼叫了定義在Dog中的run方法。

inherit_super.py
class Animal:
	@staticmethod
	def show():
		print('這裏是 Animal!')

@classmethod def eat(cls, something): print(f'Animal 吃點 {something}')
def run(self): print('Animal 奔跑起來了!')

class Cat(Animal): @staticmethod def show(): print('Cat 呼叫 Animal 的靜態方法 show') super(Cat, Cat).show()
@classmethod def eat(cls, something): print('Cat 呼叫 Animal 的類別方法 eat') super().eat(something)
def run(self): print('Cat 呼叫 Animal 的執行個體方法 run') # 相當於 super(Cat, self).run() super().run()
class Dog(Animal): def run(self): print('Dog 奔跑起來了!')
class BigDog(Dog): def run(self): print('BigDog 呼叫 Animal 的 run 方法') super(Dog, self).run()
Cat.show() Cat.eat('小魚幹') Cat().run() dog = BigDog() dog.run() # 呼叫 Dog 中的 run 方法 super(BigDog, dog).run()
Cat 呼叫 Animal 的靜態方法 show
這裏是 Animal!
Cat 呼叫 Animal 的類別方法 eat
Animal 吃點 小魚幹
Cat 呼叫 Animal 的執行個體方法 run
Animal 奔跑起來了!
BigDog 呼叫 Animal 的 run 方法
Animal 奔跑起來了!
Dog 奔跑起來了!

Python 的 super 物件可能不支援 [] 運算子

由於super類別僅是一種存取代理,因此他可能並不支援你所期望的特性存取方式。比如,在 Python 類別中定義方法__getitem__之後,可以對該類別的執行個體使用[]運算子,但對於super來說,僅可通過.運算子呼叫__getitem__,使用[]運算子將導致例外狀況TypeError

下面的類別Store雖然定義了方法__getitem__,但只能通過super.運算子來呼叫他。

inherit_getitem.py
# 一個表示商店的類別
class Store:
	def __getitem__(self, key):
		print(f'想獲得物品?{key}')
		return key

# 一個表示應用商店的類別 class AppStore(Store): pass
store = AppStore() # 可以對執行個體使用 [] 運算子 x = store['x'] # 對 super 需要使用 . 運算子 y = super(AppStore, store).__getitem__('y') # ERROR 對 super 使用 [] 運算子,將導致例外狀況 z = super(AppStore, store)['z']
想獲得物品?x
想獲得物品?y

TypeError: 'super' object is not subscriptable

程式碼

src/zh-hant/classes·codebeatme/python·GitHub