Python 抽象類別,抽象方法介紹,以及定義和實作 Python 抽象類別,抽象方法

    閱讀 15:43·字數 4716·更新 
    Youtube 頻道
    訂閱 133

    先決條件

    閱讀本節的先決條件是對 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不在其中。

    abstract.py
    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類別,語法形式如下,其中classnameblock的含義與之前相同。

    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在其中。

    abstract.py
    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類別定義了兩個抽象方法countname,他們未被衍生類別Cat實作,但通過CatAnimal來呼叫卻是可行的。

    abstract_methods.py
    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動態增加一個抽象方法showBlack類別沒有實作該方法,但建立其執行個體是可行的,因為ABCMeta並不知道新加入的抽象方法,直至通過update_abstractmethods函式更新相關的抽象類別。

    abstract_update.py
    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,因此PlantFlower均不能建立執行個體,BigFlower類別實作了Flower類別的抽象類別方法count,因此BigFlower可以建立執行個體。

    abstract_implement.py
    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.registerabstractclass是一個抽象類別。

    Python 抽象類別不會出現在其虛擬衍生類別的方法解析順序中

    雖然虛擬衍生類別可被issubclass函式判斷為 Python 抽象類別的衍生類別,虛擬衍生類別的執行個體可被isinstance函式判斷為 Python 抽象類別的執行個體,但 Python 抽象類別和其所有的抽象基底類別,不會出現在其虛擬衍生類別的方法解析順序中,這意味著你無法在虛擬衍生類別中使用super來存取 Python 抽象類別中的方法或其他特性,因為,這些抽象類別不會被 Python 搜尋。

    如何檢視 Python 虛擬衍生類別的快取權杖?

    每當任意 Python 抽象類別的register方法被呼叫時,包括作為修飾詞被呼叫的情況,Python 虛擬衍生類別的快取權杖都會發生改變,這一點可通過abc模組的get_cache_token函式進行驗證(需要 Python 3.4 以上版本)。

    下面,我們通過register方法將JerryTom註冊為抽象基底類別Man的虛擬衍生類別,並使用函式get_cache_token檢視快取權杖的變化。

    在註冊為虛擬衍生類別後,JerryTom可在實作或未實作抽象方法的情況下建立執行個體。不過,由於Man不在JerryTom的方法解析順序中,因此,Tom類別的陳述式super().run()將導致例外狀況AttributeError

    abstract_register.py
    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函式,其傳回值為TrueFalse時,issubclass函式亦將傳回TrueFalse,其傳回值為NotImplemented時,issubclass函式將根據自己原有的邏輯判斷並傳回結果。

    一旦為某個 Python 抽象類別定義了類別方法__subclasshook__,那麽__subclasshook__將適用於該抽象類別的所有衍生類別,如果他沒有被某個衍生類別覆寫的話。

    __subclasshook__(subclass)

    subclass 參數

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

    我們為下面的Unit類別定義了類別方法__subclasshook__,並傳回False,這將使得所有類別均不被認定為Unit的衍生類別,即便他們真的從Unit繼承。

    abstract_subclasshook.py
    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

    程式碼

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