iT邦幫忙

2022 iThome 鐵人賽

DAY 11
0

昨天介紹了Object-Oriented程式語言中property的基本概念,以及Python實作property的方式:property()函數。今天property()進化了,成為property decorator(註1)。


  • Python的property()可以作為decorator(中文稱為「裝飾器」)。使用decorators可快速建立類別內的properties。注意這裡用複數,意即多個或所有properties。

  • 使用property decorator和使用property()函數一樣,不必import甚麼東西,直接用即可,方便得很。

  • 將property()作為decorator,是Python社群目前的主流用法。

  • Property decorator版的Tree()類別定義:

    class Tree:
        '''Property Decorator範例'''
        def __init__(self, breed: str, age: int):   # constructor
            self.__breed = breed     # private attribute
            self.__age = age         # private attribute
    
        @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}'不正確。")
            self.__breed = breed
    
        @breed.deleter
        def breed(self):
            '''The breed property(deleter).'''
            if self.__age > 1_000:
                raise Exception('千年古樹禁止砍伐。')
            del self.__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 not isinstance(age, int):
                raise TypeError('樹齡必須是整數。')
            # 以下的條件判斷只是「示意」,實際上該和breed一併考慮才對。
            if age > 15_000 or age < 0:
                raise Exception(f'樹齡數字{age}不合理。')
            self.__age = age
    
        @age.deleter
        def age(self):
            '''The age property(deleter).'''
            del self.__age
    
  • 開始逐段說明。為求精簡僅說明breed部分,age請類推:

    1. 設定property及其getter:
        @property
        def breed(self):
            """The breed property(getter)."""
            return self.__breed   
    
    • @property就是一個decorator。意思是依憑其下的breed()方法,先創造一個同名的breed property,然後將breed()方法變成property的getter。

    1. 設定setter:
        @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('樹種名稱不正確。')
            self.__breed = breed
    
    • @breed.setter一目了然,當然是將其下的breed()變成setter。
    • 注意方法內有判斷條件。不是賦予字串、或字串不在樹種清單內,都不存入而是觸發(raise)某個Exception(註3)。

    1. 設定deleter:
        @breed.deleter
        def breed(self):
            """The breed property(deleter)."""
            if self.__age > 1_000:
                raise Exception('千年古樹禁止砍伐。')
            del self.__breed
    
    • deleter之前沒怎麼提及。顧名思義,就是讓使用者以操作普通變數的方式,用Python的del指令來刪除物件的(私有)屬性。
    • 當然要刪除前加上任何條件都悉從遵便。像本例就限制千年以上古樹不可刪除。
  • 整體說明與討論:

    • 和昨天的property()函數相比,decorator版的property清爽潔淨,更加直觀
    • 相信各位已經注意到,加上decorator之後,不管是getter, setter, 或deleter,方法名稱竟然都一模一樣(註2)。例如樹種的xx'ers都稱為breed(),樹齡則都叫age()。這點和一般Python程式同名變數或函數,後者蓋掉前者的行為模式不同。
    • 再打個比方:property好比筆者常吃的韭菜水餃和青葱水餃,表皮相同,內餡各異。表皮是使用者看得見摸得到的部分,內餡則無法從外觀看出,吃進去方知不同。不知這個比喻是否恰當。

  • 主程式:

    try:
        tree = Tree('cedar', 1_250)   # 建立物件時設定初值:雪松, 1250歲
        # 請記住breed是property,非attribute。
        print(f'\n物件初值:{tree.breed=:15}{tree.age=:,}')
    
        # 這行實際上是執行breed的setter。因123是整數不是字串,
        # 會觸發Exception,直接跳到finally區塊。
        tree.breed = 123      # breed型別不能是int。
    
        # 因上一行產生Exception,從下行起try block的code通通略過不跑。
        print(f'修改樹種後:{tree.breed=}')           # 不會執行。
    
        # 雖然4500是cedar的合法樹齡(cedar設定最多5000歲),可是下行根本沒執行,
        # 所以樹齡維持原樣沒有更動。
        tree.age = 4_500                              # 不會執行。
        print(f'修改樹齡後:{tree.age=:,}')           # 不會執行。
    except Exception as e:
        print(f'錯誤訊息:{str(e)}')
    finally:
        print(f'最後結果:{tree.breed=:15}{tree.age=:,}')
    
  • 由於類別中breed的setter檢查傳入參數的型別(就是if not isinstance(breed, str):這行):
    https://ithelp.ithome.com.tw/upload/images/20240115/20148485lKohSutiCH.png
    主程式在賦值時用tree.breed = 123,型別不對,產生Exception。表示setter守土有功,攔阻了不正確資料。
    https://ithelp.ithome.com.tw/upload/images/20220926/201484851XdoswpGkU.png

  • 如果將tree.breed = 123這行改為tree.breed = 'rubber',因rubber橡膠樹不在類別設定好的樹種清單中(見下圖):
    https://ithelp.ithome.com.tw/upload/images/20240115/20148485ieRoNdeNBK.png
    因而觸發另一個Exception:
    https://ithelp.ithome.com.tw/upload/images/20220926/20148485oaHdg9hYV9.png

  • 賦予正確的樹種,才更改成功:

    try:
        tree = Tree('cedar', 1_250)   # 建立物件時設定初值:雪松, 1250歲
        # 請記住breed是property,非attribute。
        print(f'\n物件初值:{tree.breed=:15}{tree.age=:,}')
    
        # 這行實際上是執行breed的setter。
        tree.breed = 'maple'     # 'maple'有在樹種清單中。
        print(f'修改樹種後:{tree.breed=}')
    except Exception as e:
        print(f'錯誤訊息:{str(e)}')
    finally:
        print(f'最後結果:{tree.breed=:15}{tree.age=:,}')
    

    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220926/2014848568eUz98E2A.png

  • 至於修改樹種後,新樹種超過其樹齡上限(maple楓樹設定最多只有500歲),是另一個問題。表示setter的邏輯還不夠嚴謹,要回頭加強setter的判斷條件。以下是可能的改善方法之一:

        @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
    

    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220926/201484850TZT6TjlRs.png

這樣即可攔阻存入新樹種後,和其樹齡不匹配問題。

今天到此為止,明天繼續講property的剩餘部分。


註1: Decorator(裝飾器)是Python的「特異功能」之一。不過和本系列主題較無關係。簡述如下:Decorator是Python的一種功能強大機制(有人稱之為設計模式design pattern)。它可以讓使用者針對某個已存在的結構(例如函數、類別)「外掛」功能,而不須修改結構內部的程式碼。Decorator通常位於它欲「裝飾」的函數、類別之前,而且必須前綴@符號以資識別。

註2: 物件導向領域中,有另一術語曰overloading,其中譯筆者愛用「同名異式」。但細心觀察,Python的property decorator並非overloading。至於overloading的具體內容,容後詳述。

註3: Exception的中文台灣多譯為「例外」,筆者則傾向使用「異常」。


上一篇
Attributes vs Properties
下一篇
Property的應用Scenario
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言