Python 抽象類別,抽象方法介紹,以及定義和實作 Python 抽象類別,抽象方法
先決條件
閱讀本節的先決條件是對 Python 類別繼承以及類別的多型有所掌握,你可以檢視Python 類別繼承介紹,以及實作 Python 類別繼承,多重繼承,方法覆寫,程式設計教學的類別的多型,方法覆寫,方法多載,方法遮蔽介紹來了解相關資訊。
Python 中的介面
你不能像在其他語言(比如,C#)中一樣,使用類似於interface
這樣的關鍵字,為 Python 定義介面(Interface)。Python 中的“介面”僅是概念性詞匯,用於表示一種功能實作上的約定。
介面
想要了解更多關於介面的資訊,你可以檢視程式設計教學的介面,隱含實作,明確實作介紹一節。
定義 Python 抽象類別
雖然無法定義介面,但 Python 抽象類別可以達到與介面近似的效果,他能夠約束其衍生類別必須通過覆寫(Override)來實作某些方法或特性。
同樣的,Python 沒有提供類似於abstract
這樣的關鍵字,用於宣告一個抽象類別或抽象基底類別(Abstract Base Classe,ABC),Python 抽象基底類別的定義需要通過指定元類別(Meta Class)來完成,任何指定abc
模組的ABCMeta
類別為元類別的類別,均可被稱為 Python 抽象基底類別。指定元類別需要使用關鍵字metaclass
,因此,定義 Python 抽象基底類別的語法如下。
class <classname>(<metaclass=ABCMeta>):
<block>
- classname 部分
classname
為抽象基底類別的名稱,他需要符合 Python 的識別碼規格,不能使用 Python 關鍵字或保留關鍵字。- block 部分
block
為抽象基底類別的主體程式碼,需要使用某種空白字元進行縮排,以表示其歸屬於抽象基底類別。
Python abc 模組的 ABCMeta 類別
abc
模組的ABCMeta
類別繼承自type
,可以作為其他類別的元類別,來實作抽象基底類別的效果。Python 元類別通常用於在執行個體的建立過程中施加更多控製,ABCMeta
類別的作用之一便是檢查是否所有的抽象方法或特性均被覆寫,如果沒有,則執行個體無法被建立。除此之外,ABCMeta
並不會過多的在其他方面影響類別,你可以在 Python 抽象類別中進行一些常規操作,比如,定義並使用非抽象的執行個體方法,類別方法等。
需要指出,ABCMeta
類別不會出現在 Python 抽象基底類別和其衍生類別的方法解析順序(Method Resolution Order,MRO)中,ABCMeta
類別不是 Python 抽象基底類別的基底類別。
下面,我們將ABCMeta
指定為類別Movable
的元類別,檢視抽象基底類別Movable
的方法解析順序和基底類別,發現ABCMeta
不在其中。
from abc import ABCMeta
# 表示可移動的抽象基底類別
class Movable(metaclass=ABCMeta):
pass
# 顯示 Movable 的方法解析順序和基底類別
print(Movable.__mro__)
print(Movable.__bases__)
(<class '__main__.Movable'>, <class 'object'>)
(<class 'object'>,)
當然,使用關鍵字metaclass
可能會讓人感覺怪怪的,Python 提供了另一種定義抽象基底類別的方式,直接繼承abc
模組的ABC
類別,語法形式如下,其中classname
和block
的含義與之前相同。
class <classname>(ABC):
<block>
Python abc 模組的 ABC 類別
abc
模組的ABC
類別是一個輔助類別(Helper Class),他將ABCMeta
指定為元類別,從ABC
衍生的 Python 類別會成為抽象基底類別。你不能像使用ABCMeta
類別一樣,通過metaclass
關鍵字來使用ABC
,那將導致一些問題。
與ABCMeta
類別不同,ABC
會出現在 Python 抽象基底類別和其衍生類別的方法解析順序中,ABC
類別是 Python 抽象基底類別的基底類別。
下面,我們從ABC
衍生Accessible
類別,檢視抽象基底類別Accessible
的方法解析順序和基底類別,發現ABC
在其中。
from abc import ABC
# 表示可存取的抽象基底類別
class Accessible(ABC):
pass
# 顯示 Accessible 的方法解析順序和基底類別
print(Accessible.__mro__)
print(Accessible.__bases__)
(<class '__main__.Accessible'>, <class 'abc.ABC'>, <class 'object'>)
(<class 'abc.ABC'>,)
為 Python 抽象類別定義抽象方法
在 Python 抽象基底類別或其衍生類別中,你可以使用abc
模組提供的@abstractmethod
修飾詞,來定義抽象方法或本質上是方法的抽象特性。@abstractmethod
修飾詞可與其他修飾詞(@classmethod
,@staticmethod
,@property
等)一起使用,來構成更為複雜的方法定義,比如,抽象的類別方法,抽象的靜態方法,抽象屬性等。
Python 3.2 曾經加入了一些其他關於抽象的修飾詞,但在 Python 3.3 中被取代,他們是@abstractclassmethod
,@abstractstaticmethod
,@abstractproperty
。
ABCMeta
會檢查從抽象基底類別衍生的 Python 類別,是否覆寫了繼承鏈中所有已定義的抽象方法或特性,當該 Python 類別的執行個體被建立時。
Python abc 模組的 @abstractmethod 修飾詞
@abstractmethod
修飾詞的主要目的是標記 Python 抽象方法,以讓ABCMeta
類別對這些方法進行檢查,他並不影響被標記方法的其他方面。比如,被標記為抽象方法的方法可擁有自己的主體程式碼,並且可通過super
在衍生類別中存取。
@abstractmethod
修飾詞應書寫在相關修飾詞(@classmethod
,@staticmethod
,@property
等)之後,以避免引發例外狀況AttributeError
。
Python 抽象類別方法,抽象靜態方法可在未實作的狀態下被呼叫
由於ABCMeta
僅在建立執行個體時,對 Python 抽象方法或特性的實作情況進行檢查,因此,Python 抽象類別方法和抽象靜態方法,或本質上是方法的抽象類別特性,可以在未被實作的情況下直接呼叫,只要此過程不建立相關的 Python 執行個體。
在下面的程式碼中,Animal
類別定義了兩個抽象方法count
和name
,他們未被衍生類別Cat
實作,但通過Cat
或Animal
來呼叫卻是可行的。
from abc import ABCMeta, abstractmethod
class Animal(metaclass=ABCMeta):
# 抽象類別方法 count
@classmethod
@abstractmethod
def count(cls):
pass
# 抽象靜態方法 name
@staticmethod
@abstractmethod
def name():
print('名字到底是什麽啊?')
class Cat(Animal):
pass
# 呼叫未實作的靜態方法 name 和類別方法 count
Cat.name()
Animal.count()
# ERROR Cat 並未實作抽象基底類別 Animal
Cat().name()
名字到底是什麽啊?
…
TypeError: Can't instantiate abstract class Cat without an implementation for abstract methods 'count', 'name'
使用 update_abstractmethods 函式更新 Python 抽象類別
abc
模組的update_abstractmethods
函式(支援作為類別的修飾詞來使用),可用於更新一個 Python 抽象類別,當該類別動態新增抽象特性,或抽象特性的狀態發生變化時。
需要指出,update_abstractmethods
函式只更新目標抽象類別,並不更新目標抽象類別的基底類別或衍生類別。
update_abstractmethods(cls)
- cls 參數
cls
參數為需要更新的抽象類別。
接下來,我們為抽象基底類別Color
動態增加一個抽象方法show
,Black
類別沒有實作該方法,但建立其執行個體是可行的,因為ABCMeta
並不知道新加入的抽象方法,直至通過update_abstractmethods
函式更新相關的抽象類別。
from abc import ABC, abstractmethod, update_abstractmethods
# 定義抽象基底類別 Color 和衍生類別 Black
class Color(ABC):
pass
class Black(Color):
pass
# 動態的為抽象基底類別增加一個抽象方法
@abstractmethod
def show(self):
pass
Color.show = show
print('建立第一個 Black 執行個體')
# 建立執行個體沒有問題
Black()
# 更新抽象類別 Color 和 Black
update_abstractmethods(Color)
update_abstractmethods(Black)
print('建立第二個 Black 執行個體')
# ERROR 不能建立執行個體,因為 Black 沒有實作抽象方法 show
Black()
建立第一個 Black 執行個體
建立第二個 Black 執行個體
…
TypeError: Can't instantiate abstract class Black without an implementation for abstract method 'show'
實作 Python 抽象類別,抽象方法
正如上面所描述的,對已經定義的 Python 抽象方法或本質為方法的抽象特性進行覆寫,意味著實作該抽象方法或特性。當 Python 抽象類別中的所有抽象方法和特性均被實作時,即表示該抽象類別被實作。
覆寫 Python 抽象方法或特性不能使用修飾詞 @abstractmethod
在覆寫 Python 抽象方法或特性時,不能為其再次書寫修飾詞@abstractmethod
,否則該方法或特性將被認定為抽象定義,而不是對抽象的實作。
在下面的範例中,Flower
類別實作了Plant
類別的抽象方法grow
,並定義了自己的抽象類別方法count
,因此Plant
和Flower
均不能建立執行個體,BigFlower
類別實作了Flower
類別的抽象類別方法count
,因此BigFlower
可以建立執行個體。
from abc import ABCMeta, abstractmethod
class Plant(metaclass=ABCMeta):
# 定義抽象方法 grow
@abstractmethod
def grow(self):
print('Plant 生長吧!')
class Flower(Plant):
# 覆寫抽象方法 grow,以實作他
@abstractmethod
def grow(self):
super().grow()
# 定義抽象類別方法 count
@classmethod
@abstractmethod
def count(self):
pass
class BigFlower(Flower):
# 覆寫抽象類別方法 count,以實作他
@classmethod
def count(self):
print('一共有多少個哪?')
# 不能建立 Plant 和 Flower 的執行個體
BigFlower().grow()
Plant 生長吧!
將 Python 類別註冊為不需要實作抽象類別的虛擬衍生類別
任何 Python 抽象基底類別或其衍生類別,都擁有一個名稱為register
的方法,使用該方法可將一個不相關的類別,註冊為 Python 抽象類別的虛擬衍生類別(Virtual Subclass)。與從抽象類別直接衍生的 Python 類別不同,虛擬衍生類別可以被具現化,無論其是否已經實作了抽象類別中的所有抽象方法。
register(subclass)
- subclass 參數
subclass
參數為需要註冊為虛擬衍生類別的類別。
此外,在 Python 3.3 或更高的版本中,register
方法可以作為類別的修飾詞來使用,以將類別註冊為某個 Python 抽象類別的虛擬衍生類別。其語法格式為@abstractclass.register
,abstractclass
是一個抽象類別。
Python 抽象類別不會出現在其虛擬衍生類別的方法解析順序中
雖然虛擬衍生類別可被issubclass
函式判斷為 Python 抽象類別的衍生類別,虛擬衍生類別的執行個體可被isinstance
函式判斷為 Python 抽象類別的執行個體,但 Python 抽象類別和其所有的抽象基底類別,不會出現在其虛擬衍生類別的方法解析順序中,這意味著你無法在虛擬衍生類別中使用super
來存取 Python 抽象類別中的方法或其他特性,因為,這些抽象類別不會被 Python 搜尋。
如何檢視 Python 虛擬衍生類別的快取權杖?
每當任意 Python 抽象類別的register
方法被呼叫時,包括作為修飾詞被呼叫的情況,Python 虛擬衍生類別的快取權杖都會發生改變,這一點可通過abc
模組的get_cache_token
函式進行驗證(需要 Python 3.4 以上版本)。
下面,我們通過register
方法將Jerry
,Tom
註冊為抽象基底類別Man
的虛擬衍生類別,並使用函式get_cache_token
檢視快取權杖的變化。
在註冊為虛擬衍生類別後,Jerry
和Tom
可在實作或未實作抽象方法的情況下建立執行個體。不過,由於Man
不在Jerry
和Tom
的方法解析順序中,因此,Tom
類別的陳述式super().run()
將導致例外狀況AttributeError
。
from abc import ABC, abstractmethod, get_cache_token
# 定義抽象基底類別 Man
class Man(ABC):
@abstractmethod
def run(self):
pass
# 將類別 Jerry 和 Tom 註冊為虛擬衍生類別,並檢視權杖的變化
print(f'權杖為 {get_cache_token()}')
@Man.register
class Jerry():
pass
print(f'權杖為 {get_cache_token()}')
class Tom():
def run(self):
# ERROR Man 不在 MRO 中
super().run()
Man.register(Tom)
print(f'權杖為 {get_cache_token()}')
# Jerry 和 Tom 被認為是衍生類別
print(issubclass(Jerry, Man))
print(issubclass(Tom, Man))
# 可以正常建立執行個體
print(isinstance(Jerry(), Man))
print(isinstance(Tom(), Man))
Tom().run()
權杖為 24
權杖為 25
權杖為 26
True
True
True
True
…
AttributeError: 'super' object has no attribute 'run'
使用類別方法 __subclasshook__ 決定 Python 抽象類別的衍生類別
你可以為 Python 抽象類別定義類別方法__subclasshook__
,來決定某個類別是否為 Python 抽象類別的衍生類別。__subclasshook__
方法將影響issubclass
函式,其傳回值為True
或False
時,issubclass
函式亦將傳回True
或False
,其傳回值為NotImplemented
時,issubclass
函式將根據自己原有的邏輯判斷並傳回結果。
一旦為某個 Python 抽象類別定義了類別方法__subclasshook__
,那麽__subclasshook__
將適用於該抽象類別的所有衍生類別,如果他沒有被某個衍生類別覆寫的話。
__subclasshook__(subclass)
- subclass 參數
subclass
參數為需要判斷是否為衍生類別的類別。
我們為下面的Unit
類別定義了類別方法__subclasshook__
,並傳回False
,這將使得所有類別均不被認定為Unit
的衍生類別,即便他們真的從Unit
繼承。
from abc import ABC
class Unit(ABC):
# 所有的類別都不是 Unit 的衍生類別
@classmethod
def __subclasshook__(cls, subclass):
print(f'{subclass} 是 Unit 的衍生類別嗎?')
return False
class Player(Unit):
pass
# 檢視 Player 與 Unit 之間的關系
print(issubclass(Player, Unit))
print(isinstance(Player(), Unit))
<class '__main__.Player'> 是 Unit 的衍生類別嗎?
False
False