Python 抽象類別,抽象方法介紹,以及定義和實作抽象類別,抽象方法
訂閱 375
先決條件
閱讀本節的先決條件是對 Python 類別繼承以及類別的多型有所掌握,你可以檢視Python 類別繼承介紹,以及實作類別繼承,多重繼承,方法覆寫,程式設計教學的類別的多型,方法覆寫,方法多載,方法遮蔽介紹來了解相關資訊。
Python 中的介面
你不能像在其他語言(比如,C#)中一樣,使用類似於interface
這樣的關鍵字,為 Python 定義介面(Interface)。Python 中的「介面」僅是概念性詞彙,用於表示一種功能實作上的約定。
介面
想要了解更多關於介面的資訊,你可以檢視程式設計教學的介面,隱含實作,明確實作介紹一節。
定義 Python 抽象類別
雖然無法定義介面,但抽象類別可以達到與介面近似的效果,他能夠約束其衍生類別必須通過覆寫(Override)來實作某些方法或特性。
同樣的,Python 沒有提供類似於abstract
這樣的關鍵字,用於宣告一個抽象類別或抽象基底類別(Abstract Base Classe,ABC),Python 抽象基底類別的定義需要通過指定元類別(Meta Class)來完成,任何指定abc
模組的ABCMeta
類別為元類別的類別,均可被稱為抽象基底類別。指定元類別需要使用關鍵字metaclass
,因此,定義抽象基底類別的語法如下。
class <classname>(<metaclass=ABCMeta>):
<block>
- classname 部分
classname
為抽象基底類別的名稱,他需要符合 Python 的識別碼規格,不能使用關鍵字或保留關鍵字。- block 部分
block
為抽象基底類別的主體程式碼,需要使用某種空白字元進行縮排,以表示其歸屬於抽象基底類別。
abc 模組的 ABCMeta 類別
abc
模組的ABCMeta
類別繼承自type
,可以作為其他類別的元類別,來實作抽象基底類別的效果。元類別通常用於在執行個體的建立過程中施加更多控製,ABCMeta
類別的作用之一便是檢查是否所有的抽象方法或特性均被覆寫,如果沒有,則執行個體無法被建立。除此之外,ABCMeta
並不會過多的在其他方面影響類別,你可以在抽象類別中進行一些常規操作,比如,定義並使用非抽象的執行個體方法,類別方法等。
需要指出,ABCMeta
類別不會出現在抽象基底類別和其衍生類別的方法解析順序(Method Resolution Order,MRO)中,ABCMeta
類別不是抽象基底類別的基底類別。
下面,我們將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>
abc 模組的 ABC 類別
abc
模組的ABC
類別是一個輔助類別(Helper Class),他將ABCMeta
指定為元類別,從ABC
衍生的類別會成為抽象基底類別。你不能像使用ABCMeta
類別一樣,通過metaclass
關鍵字來使用ABC
,那將導致一些問題。
與ABCMeta
類別不同,ABC
會出現在抽象基底類別和其衍生類別的方法解析順序中,ABC
類別是抽象基底類別的基底類別。
下面,我們從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 抽象類別定義抽象方法
在抽象基底類別或其衍生類別中,你可以使用abc
模組提供的@abstractmethod
修飾詞,來定義抽象方法或本質上是方法的抽象特性。@abstractmethod
修飾詞可與其他修飾詞(@classmethod
,@staticmethod
,@property
等)一起使用,來構成更為複雜的方法定義,比如,抽象的類別方法,抽象的靜態方法,抽象屬性等。
Python 3.2 曾經加入了一些其他關於抽象的修飾詞,但在 Python 3.3 中被取代,他們是@abstractclassmethod
,@abstractstaticmethod
,@abstractproperty
。
ABCMeta
會檢查從抽象基底類別衍生的類別,是否覆寫了繼承鏈中所有已定義的抽象方法或特性,當該類別的執行個體被建立時。
abc 模組的 @abstractmethod 修飾詞
@abstractmethod
修飾詞的主要目的是標記抽象方法,以讓ABCMeta
類別對這些方法進行檢查,他並不影響被標記方法的其他方面。比如,被標記為抽象方法的方法可擁有自己的主體程式碼,並且可通過super
在衍生類別中存取。
@abstractmethod
修飾詞應書寫在相關修飾詞(@classmethod
,@staticmethod
,@property
等)之後,以避免引發例外狀況AttributeError
。
抽象類別方法,抽象靜態方法可在未實作的狀態下被呼叫
由於ABCMeta
僅在建立執行個體時,對抽象方法或特性的實作情況進行檢查,因此,抽象類別方法和抽象靜態方法,或本質上是方法的抽象類別特性,可以在未被實作的情況下直接呼叫,只要此過程不建立相關的執行個體。
在下面的程式碼中,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 函式更新抽象類別
abc
模組的update_abstractmethods
函式(支援作為類別的修飾詞來使用),可用於更新一個抽象類別,當該類別動態新增抽象特性,或抽象特性的狀態發生變化時。
需要指出,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 抽象類別,抽象方法
正如上面所描述的,對已經定義的抽象方法或本質為方法的抽象特性進行覆寫,意味著實作該抽象方法或特性。當抽象類別中的所有抽象方法和特性均被實作時,即表示該抽象類別被實作。
覆寫抽象方法或特性不能使用修飾詞 @abstractmethod
在覆寫抽象方法或特性時,不能為其再次書寫修飾詞@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 類別註冊為不需要實作抽象類別的虛擬衍生類別
任何抽象基底類別或其衍生類別,都擁有一個名稱為register
的方法,使用該方法可將一個不相關的類別,註冊為抽象類別的虛擬衍生類別(Virtual Subclass)。與從抽象類別直接衍生的類別不同,虛擬衍生類別可以被具現化,無論其是否已經實作了抽象類別中的所有抽象方法。
register(subclass)
- subclass 參數
subclass
參數為需要註冊為虛擬衍生類別的類別。
此外,在 Python 3.3 或更高的版本中,register
方法可以作為類別的修飾詞來使用,以將類別註冊為某個抽象類別的虛擬衍生類別。其語法格式為@abstractclass.register
,abstractclass
是一個抽象類別。
抽象類別不會出現在其虛擬衍生類別的方法解析順序中
雖然虛擬衍生類別可被issubclass
函式判斷為抽象類別的衍生類別,虛擬衍生類別的執行個體可被isinstance
函式判斷為抽象類別的執行個體,但抽象類別和其所有的抽象基底類別,不會出現在其虛擬衍生類別的方法解析順序中,這意味著你無法在虛擬衍生類別中使用super
來存取抽象類別中的方法或其他特性,因為,這些抽象類別不會被搜尋。
如何檢視虛擬衍生類別的快取權杖?
每當任意抽象類別的register
方法被呼叫時,包括作為修飾詞被呼叫的情況,虛擬衍生類別的快取權杖都會發生改變,這一點可通過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__ 決定抽象類別的衍生類別
你可以為抽象類別定義類別方法__subclasshook__
,來決定某個類別是否為抽象類別的衍生類別。__subclasshook__
方法將影響issubclass
函式,其傳回值為True
或False
時,issubclass
函式亦將傳回True
或False
,其傳回值為NotImplemented
時,issubclass
函式將根據自己原有的邏輯判斷並傳回結果。
一旦為某個抽象類別定義了類別方法__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