昨天介紹了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請類推:
@property
def breed(self):
"""The breed property(getter)."""
return self.__breed
@property
就是一個decorator。意思是依憑其下的breed()
方法,先創造一個同名的breed
property,然後將breed()
方法變成property的getter。 @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。 @breed.deleter
def breed(self):
"""The breed property(deleter)."""
if self.__age > 1_000:
raise Exception('千年古樹禁止砍伐。')
del self.__breed
整體說明與討論:
breed()
,樹齡則都叫age()
。這點和一般Python程式同名變數或函數,後者蓋掉前者的行為模式不同。主程式:
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):
這行):
主程式在賦值時用tree.breed = 123
,型別不對,產生Exception。表示setter守土有功,攔阻了不正確資料。
如果將tree.breed = 123
這行改為tree.breed = 'rubber'
,因rubber橡膠樹不在類別設定好的樹種清單中(見下圖):
因而觸發另一個Exception:
賦予正確的樹種,才更改成功:
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=:,}')
輸出:
至於修改樹種後,新樹種超過其樹齡上限(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
輸出:
這樣即可攔阻存入新樹種後,和其樹齡不匹配問題。
今天到此為止,明天繼續講property的剩餘部分。
註1: Decorator(裝飾器)是Python的「特異功能」之一。不過和本系列主題較無關係。簡述如下:Decorator是Python的一種功能強大機制(有人稱之為設計模式design pattern)。它可以讓使用者針對某個已存在的結構(例如函數、類別)「外掛」功能,而不須修改結構內部的程式碼。Decorator通常位於它欲「裝飾」的函數、類別之前,而且必須前綴@
符號以資識別。
註2: 物件導向領域中,有另一術語曰overloading
,其中譯筆者愛用「同名異式」。但細心觀察,Python的property decorator並非overloading。至於overloading的具體內容,容後詳述。
註3: Exception的中文台灣多譯為「例外」,筆者則傾向使用「異常」。