iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0

今天起介紹物件導向程式的第二大支柱:繼承(Inheritance)。不過在正式開始前,先補充一下昨天的封裝本來要講卻漏掉的部分:

封裝保護層級(public vs private)總結

以下是筆者個人看法:

  • 如果類別確定只有自己在用,那麼保護層級就較無所謂,屬性全部設定為公開未嘗不可。
  • 專案不大,團隊成員不多時,也不一定要嚴格遵守封裝保護原則,將類別封得密密實實的。
  • 但如果是較大型的專案,或者開發的類別要公開發布,那就一定得嚴格保護類別內的attributes,善加利用properties。

最簡單的繼承

  • 猜想有耐心看到本篇的讀者,對繼承應該都有一定程度了解。不過筆者還是得「照表操課」,從基礎說起。
  • 沿用之前的Tree類別,假設我們必須將樹再區分為闊葉樹(hardwood)和針葉樹(conifer)兩種。它們都是樹,但有以下不同特性(註1):
    • 闊葉樹通常是落葉植物,所以有開始落葉和全部脫落的日期屬性,即幾月開始落葉,幾月完全脫落;而針葉樹通常是常綠植物,並無落葉期屬性。至於有部分針葉樹一年四季都會有一點點落葉,就不理算了。
    • 闊葉樹是開花的種子植物,所以會有花期屬性,即幾幾月開始開花,幾月花季結束;針葉樹是不開花的種子植物,不需花期屬性。
    • 針葉樹因商業價值較高,要增加市場價格屬性以及木材外銷交易等方法;而闊葉樹則無此屬性與方法。
  • 針對以上分析,我們可以新增HardwoodConifer兩個類別(註2),讓這兩個新增類別都歸屬於Tree。用物件導向語言來講,就是HardwoodConifer兩個類別都繼承自Tree
  • 有了繼承關係後,Tree通常稱為「父類別」(parent class, base class, super class),而HardwoodConifer則是「子類別」(child class, derived class, sub class)。
  • 子類別自動擁有父類別的屬性和方法。子類別也可以新增專屬於它自己的屬性及方法
  • 類別設計如下,子類別先不實作內容,看效果如何:
    class Tree():
        __count = 0         
    
        def __init__(self, breed: str, age: int, height: int):   # constructor
            self.__breed = breed
            self.__age = age
            self.__height = height
            Tree.__count += 1
    
        @classmethod
        @property
        def count(cls) -> int:
            '''The __count property(getter).'''
            return cls.__count
    
        @property
        def breed(self) -> str:
            '''The breed property(getter).'''
            return self.__breed
    
        @breed.setter
        def breed(self, breed: str):
            '''The breed property(setter).'''
            if not isinstance(breed, str):
                raise TypeError('樹種必須是字串。')
    
            breeds = {'cedar': (0, 5_000), 'oak': (0, 300), 
                      'beech': (0, 400), 'camphor': (0, 800), 
                      'maple': (0, 500), 'phoebe': (0, 2_000)}
            breed = breed.strip().lower()
            if breed not in breeds:
                raise Exception(f"樹種名稱'{breed}'不正確。")
            min_age = breeds.get(breed)[0]
            max_age = breeds.get(breed)[1]
            if self.__age < min_age or self.__age > max_age:
                raise Exception(f"新樹種名稱'{breed}'和其年齡不匹配。")
            self.__breed = breed
    
        @property
        def age(self) -> int:   
            '''The age property(getter).'''
            return self.__age
    
        @age.setter
        def age(self, age: int):
            '''The age property(setter).'''
            if isinstance(age, bool) or not isinstance(age, int):
                raise TypeError('樹齡必須是整數。')
            # 以下的條件判斷只是「示意」,實際上該和breed一併考慮才對。
            if age > 15_000 or age < 0:
                raise Exception(f'樹齡數字{age}不合理。')
            self.__age = age
    
        @property
        def height(self) -> int:   
            '''The height property(getter).'''
            return self.__height
    
        @height.setter
        def height(self, height: int):
            '''The height property(setter).'''
            if isinstance(height, bool) or not isinstance(height, int):
                raise TypeError('樹高必須是整數。')
            if height > 200 or height < 1:
                raise Exception(f'樹高數字{height}不合理。')
            self.__height = height
    
        def show_info(self):
            print(f'{self.breed=:10}{self._age=:<10,}{self.height=:<10}')
    
    
    class Hardwood(Tree):   # 繼承自Tree類別(註3)。
        ...   # 暫不實作。
    
    
    class Conifer(Tree):    # 繼承自Tree類別。
        ...   # 暫不實作。
    
  • 測試程式:
    def show_tree_info(breed: str, age: int, height: int):
        print(f'{breed=:12}{age=:<8,}{height=:<10,}')
    
    try:
        tree1 = Hardwood('maple', 60, 30)
        tree2 = Conifer('cedar', 1_500, 96)
    
        show_tree_info(tree1.breed, tree1.age, tree1.height)
        show_tree_info(tree2.breed, tree2.age, tree2.height)
        tree1.breed = 'camphor'   # 'camphor'是合法樹種名稱,應該賦值成功。
    except Exception as e:
        print(str(e))
    finally:
        print()
        show_tree_info(tree1.breed, tree1.age, tree1.height)
    
  • 輸出:
    https://ithelp.ithome.com.tw/upload/images/20221004/20148485EevabPhmRB.png
  • 如果試圖賦予不合法的樹種(breed)名稱:
    def show_tree_info(breed: str, age: int, height: int):
        print(f'{breed=:12}{age=:<8,}{height=:<10,}')
    
    try:
        tree1 = Hardwood('maple', 60, 30)
        tree2 = Conifer('cedar', 1_500, 96)
    
        show_tree_info(tree1.breed, tree1.age, tree1.height)
        show_tree_info(tree2.breed, tree2.age, tree2.height)
        tree1.breed = 'breadfruit'   # 'breadfruit'是不合法的樹種名稱。
    except Exception as e:
        print(str(e))
    finally:
        print()
        show_tree_info(tree1.breed, tree1.age, tree1.height)
    
  • 父類別的Property breed發揮了攔阻作用,不合法的樹種名稱'breadfruit'無法寫入:
    https://ithelp.ithome.com.tw/upload/images/20221004/20148485VLOLkq6MHf.png
  • 以上的code,子類別完全沒有實作自己的商業邏輯。它們之可以順利存取breed, ageheight,原因是子類別透過繼承機制,取得了其父類別的「遺產」(註4)。這裡的遺產就是屬性(attributes and properties)和方法。

擴充子類別功能

  • 剛才展示了最基本的繼承。不過如果子類別完全不實作,就毫無意義。好,現在讓我們實作兩個子類別的擴充功能。子類別重新設計如下:
    class Hardwood(Tree):
      # 這次子類別擁有自己的constructor了。
      def __init__(self, breed: str, age: int, height: int, defoliation: dict, puberty: dict):   # defoliation是落葉,puberty是開花期。
          super().__init__(breed, age, height)  # 呼叫父類別的constructor。
          self.defoliation = defoliation   # 子類別自己的屬性,暫設為public。
          self.puberty = puberty           # 子類別自己的屬性,暫設為public。
    
    class Conifer(Tree):
      def __init__(self, breed: str, age: int, height: int, price: int):
          super().__init__(breed, age, height)  # 呼叫父類別的constructor。
          self.price = price      # 子類別自己的屬性,暫設為public。
    
      def sell(self, seller: str, buyer) -> bool:   # 子類別自己的方法。
        ...   # 實作略
        print(f'{seller=:20}{buyer=:20}')
        return True
    
  • 這次子類別有了自己的建構子。不過子類別一旦實作自己的建構子,其父類別的建構子就遭覆寫(overriding,或稱重寫)。為了不浪費父類別辛苦寫好的code,我們以super().__init__(breed, age, height)這行來手動呼叫父類別的建構子,super()代表父類別。經過手動呼叫,父類別的建構子就可以為子類別「重用」(reuse)。
  • 測試程式:
    try:
        tree1 = Hardwood('maple', 60, 30, {'start': 'Oct', 'end': 'Feb'}, {'start': 'May', 'end': 'Jul'})
        tree2 = Conifer('cedar', 1_500, 96, 2_500)
    
        print(f"{tree1.breed=:12}{tree1.age=:<8}{tree1.height=:<10,}\n{tree1.defoliation['start']=:8}{tree1.defoliation['end']=:8}\n{tree1.puberty['start']    =:8}{tree1.puberty['end']    =:8}")
        print()
        print(f"{tree2.breed=:12}{tree2.age=:<8}{tree2.height=:<10,}\n{tree2.price=:<10}")
        print('呼叫tree2.sell():')
        tree2.sell('Alex', 'Mirror')
    except Exception as e:
        print(str(e))
    
  • 輸出:
    https://ithelp.ithome.com.tw/upload/images/20221004/201484856ynU7uJVI6.png
  • 現在兩個子類別都成功擁有(或稱重用)其父類別Tree的資源和商業邏輯,同時也建立了自己的小家庭。

註1: 本系列文章目的是物件導向程式設計介紹,並非植物學研究。像闊葉樹和針葉樹實際的不同特性,當然不會像本文說的那麼絕對。請讀者不要在這方面挑筆者的骨頭。

註2: 按PEP 8規範,類別的命名建議使用Pascal Case方式(詳見本系列Day 4文),所以是HardwoodConifer而非hardwoodconifer。另外,hardwood是單字,不能作HardWood

註3: 無繼承時,類別名稱後可不加小括號()。有繼承時則一定要有(),裡面的參數是其父類別。
筆者習慣即使是無繼承,類別名稱的後面也加上(),以保持和有繼承時的語法一致,也和函數語法一致。函數沒有參數時不也要加小括號?

註4: 「遺產」一詞只是筆者一時興起的比喻,實際上子類別可是「椿萱並茂」。


上一篇
大觀園的妙玉:Static Methods
下一篇
子類別存取父類別的私有屬性和方法,真的不行嗎?
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言