iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0

今天要談的ABC不是American Born Chinese的'ABC',而是Python的一個模組:abstract base classes,以及其背後的abstract classes觀念。


Abstract Classes的定義和長相

  • 在物件導向程式設計的繼承體系中,有時候父類別定義了一些方法,卻不實作,要將這些方法的內容留給繼承它的子類別來「完成遺願」。父類別這些「只有包子的皮而無包子的餡」的方法,稱為「抽象方法」(abstract methods)。

  • 類別中只要有一個抽象方法,該類別就自動成為「抽象類別」(abstract class)。

  • 抽象類別(方法)可以視作其子類別的「藍圖」(blueprint)。它「規範」了子類別必須要有某些方法,而且最好實作這些方法。

  • 好些物件導向程式語言都有抽象類別機制。然而Python並未在語法層面直接支援,而交由一個在本篇導言提及的abstract base classes模組實作,這個模組內有一個方法,名稱就叫ABC(注意全部大寫)。

  • abc模組以裝飾器@abstractmethod來「裝飾」父類別中的抽象方法。

  • 以下程式仍以Tree為父類別,共有四個子類別,分別是Hardwood, Conifer, Arbor, 和Shrub。先看類別設計的部分:

    from abc import ABC, abstractmethod
    
    class Tree(ABC):      # 這裡的「樹」採廣義,包括灌木(shrub)。
        def __init__(self, breed: str):
            self.__breed = breed
    
        @property
        def breed(self):
            return self.__breed
    
        @abstractmethod
        def provide_food(self):
            ...
    
        @abstractmethod
        def help_breathe(self):
            ...
    
        @abstractmethod
        def conserve_water(self):
            ...
    
    
    class Hardwood(Tree):    # 闊葉樹
        # overriding abstract method
        def provide_food(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I provide food.')
    
        # overriding abstract method
        def help_breathe(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I help people breathe.')
    
        # overriding abstract method
        def conserve_water(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I conserve water.')
    
    
    class Conifer(Tree):    # 針葉樹
        # overriding abstract method
        def provide_food(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I provide food.')
    
        # overriding abstract method
        def help_breathe(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I help people breathe.')
    
        # overriding abstract method
        def conserve_water(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I conserve water.')
    
    
    class Arbor(Tree):    # 喬木
        # overriding abstract method
        def provide_food(self):
            print(f'Hi, I am an {__class__.__name__} {self.breed}.  I provide food.')
    
        # overriding abstract method
        def help_breathe(self):
            print(f'Hi, I am an {__class__.__name__} {self.breed}.  I help people breathe.')
    
        # overriding abstract method
        def conserve_water(self):
            print(f'Hi, I am an {__class__.__name__} {self.breed}.  I conserve water.')
    
    
    class Shrub(Tree):     # 灌木
        # overriding abstract method
        def provide_food(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I provide food.')
    
        # overriding abstract method
        def help_breathe(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I help people breathe.')
    
        # overriding abstract method
        def conserve_water(self):
            print(f'Hi, I am a {__class__.__name__} {self.breed}.  I conserve water.')
    
  • 說明:

    • abc不須pip install,直接用之可也。
    • from abc import ABC, abstractmethod
    • 父類別Tree須繼承自ABC(所以ABC顯然也是個類別)。
    • Tree定義了三個方法:provide_food(), help_breathe(), 和conserve_water()。這三個方法都沒有實作(內容是...Ellipsis)。
    • 三個方法都加了@abstractmethod裝飾器,成為抽象方法。
    • 四個繼承自Tree的子類別,都以筆者之前介紹過的overriding技術,「覆寫」了父類別的抽象方法。
  • 測試程式如下:

    tree1 = Hardwood('teak')
    tree1.provide_food()
    tree1.help_breathe()
    tree1.conserve_water()
    print()
    tree2 = Conifer('cedar')
    tree2.provide_food()
    tree2.help_breathe()
    tree2.conserve_water()
    print()
    tree3 = Arbor('banyan')
    tree3.provide_food()
    tree3.help_breathe()
    tree3.conserve_water()
    print()
    tree4 = Shrub('jasmine')
    tree4.provide_food()
    tree4.help_breathe()
    tree4.conserve_water()
    
  • 輸出如下:
    https://ithelp.ithome.com.tw/upload/images/20221009/20148485tE7KKbsQ3d.png

為甚麼需要Abstract Classes?

  • 父類別定義了抽象類別,就好比定義了一組Application Program Interface(API),規範子類別必須遵守。
  • API規範好之後,外界third-party第三方軟體(例如外掛程式等)就有了設計準則,只要依照準則實作出內容即可。
  • 大型專案中,定義抽象類別可以幫助團隊成員不致遺漏某些方法。

父類別不加@abstractmethod會怎樣?

  • 其實父類別也可以不加@abstractmethod裝飾器:

    from abc import ABC, abstractmethod
    
    class Tree(ABC):      # 這裡的「樹」採廣義,包括灌木(shrub)。
        def __init__(self, breed: str):
            self.__breed = breed
    
        @property
        def breed(self):
            return self.__breed
    
        # @abstractmethod
        def provide_food(self):
            ...
    
        # @abstractmethod
        def help_breathe(self):
            ...
    
        # @abstractmethod
        def conserve_water(self):
            ...
    
  • 不加@abstractmethod裝飾器的版本,上面的測試程式的結果是一樣的。

  • 那麼這個裝飾器作用何在?以目前筆者粗淺的了解(肯定有遺漏地方),如果所有方法都不加裝裝飾(也就是該類別不是抽象類別),下面的程式碼:

    tree = Tree('cedar')
    print(f'{tree.breed = }')
    

    會輸出:
    https://ithelp.ithome.com.tw/upload/images/20221009/20148485TW17d9PHiX.png

  • 但只要父類別中有任一方法宣告了抽象,例如下面的provide_food()方法:

    from abc import ABC, abstractmethod
    
    class Tree(ABC):      # 這裡的「樹」採廣義,包括灌木(shrub)。
        def __init__(self, breed: str):
            self.__breed = breed
    
        @property
        def breed(self):
            return self.__breed
    
        @abstractmethod
        def provide_food(self):
            ...
    
        # @abstractmethod
        def help_breathe(self):
            ...
    
        # @abstractmethod
        def conserve_water(self):
            ...
    

    跑同樣的測試碼時:

    try: 
        tree = Tree('cedar')
        print(f'{tree.breed = }')
    except Exception as e:
        print(str(e))
    

    會產生以下的異常(exception):
    https://ithelp.ithome.com.tw/upload/images/20221009/20148485UG6Qr5cTAv.png

  • 也就是說,一旦成為抽象類別,就只能被別人繼承,自己無法建立實例了。


上一篇
The Pros and Cons of Multiple Inheritance
下一篇
還是Abstract...
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言