今天介紹一個Python在設計類別時的小工具dataclass
模組,大概勉強和封裝攀得上一點關係。一表三千里嘛。
之前我們看到的類別設計範例,其實只能算是簡易入門版。真要設計一個Python的完整類別,必須考慮更多才夠周延。這代表要寫更多的code。除了getters, setters, deleters及其他和商業邏輯有關的方法之外,最好還要加上一些稱為boilerplate code
(註1)的方法。
為了講解方便,以下這段code姑且名為「傳統版」。請注意那幾個__xx__()
魔術方法,那些就是剛剛說的boilerplate code
。這個術語就直接用英文,不中譯了。
class Tree():
'''為了聚焦本class僅實作getters,略去setters, deleters。'''
def __init__(self, breed: str, age: int, height: int):
self.__breed = breed
self.__age = age
self.__height = height
@property
def breed(self) -> str:
'''The breed property(getter).'''
return self.__breed
@property
def age(self) -> int:
'''The age property(getter).'''
return self.__age
@property
def height(self) -> int:
'''The height property(getter).'''
return self.__height
# 真要設計完整的class,還得實作以下的boilerplate code。
def __repr__(self) -> str:
return f'Tree({self.breed=}, {self.age=}, {self.height=})'
def __hash__(self):
return hash((self.__class__, self.breed, self.age, self.height))
def __eq__(self, other):
if other.__class__ is self.__class__:
return self.breed == other.breed and self.age == other.age and self.height == other.height
else:
return NotImplemented
再嘮叨一下:上面的魔術方法__repr__()
, __hash__()
, __eq__()
,還有沒寫到的__new__()
, __str__()
, __lt__()
, __gt__()
...等等(族繁不及備載),就是boilerplate code
。
boilerplate code形式和內容比較公式化,但有時沒它又不行,一些功能會出不來。還有,這些Python類別的boilerplates有些是必需品,有些大概可有可無(optional),視需求而定。
檢查一下:
tree = Tree('beech', 260, 307)
print(f'\n{tree}\n')
「傳統版」輸出:
表示__repr__()
code正確無誤。
不過,每次設計類別,都得複製貼上這些boilerplate程式碼再略加修改,也屬煩事一椿。如能偷懶不寫該有多好...
懶惰為發明之母。dataclass大概就在這個需求下,於Python 3.7版時降生了。
上面「傳統版」的code,改套dataclass後長這樣子。無以名之,且稱「簡潔版」:
from dataclasses import dataclass
@dataclass # 這裡要加一個decorator。
class Tree():
'''使用dataclass時不用寫那幾個boilerplates。dataclass會自動幫我們產生。'''
__breed: str # 表面上已沒有constructor,只有私有屬性的名稱。
__age: int # 語法非常簡潔。
__height: int
@property
def breed(self) -> str:
'''The breed property(getter).'''
return self.__breed
@property
def age(self) -> int:
'''The age property(getter).'''
return self.__age
@property
def height(self) -> int:
'''The height property(getter).'''
return self.__height
# 這時不用寫那幾個boilerplates了。dataclass會自動幫我們產生。
tree = Tree('beech', 260, 307)
print(f'\n{tree}\n')
「簡潔版」輸出:
套用datacalss模組的「簡潔版」程式碼是不是清爽多了?注意:以上的程式碼並沒有實作__repr__()
以及其他的__xx__()
,那些boilerplates是由dataclass幫忙自動產生的。
再貼一次「傳統版」的輸出以資比較:
兩者內容基本上一樣。不過「傳統版」因為是自行撰寫,要寫甚麼都可以。而由dataclass產生的「簡潔版」,彈性也許沒那麼大,內容和形式都套模板,所以是固定的。
__post_init__()
方法dataclass模組還額外提供__post_init()
方法。是在執行完建構子__init__()
之後自動執行的方法。本例是利用__post_init()
來查出每棵樹在植物分類學上屬於哪一個family,即「界|門|綱|目|科|屬|種」中的「科」:
from dataclasses import dataclass
@dataclass
class Tree():
'''依然不用寫那些boilerplates。'''
__breed: str
__age: int
__height: int
def __post_init__(self): # 這是dataclass模組提供的方法。
'''查出每個breed(一般用語的樹種)在植物分類學上屬於哪一個family(科)。'''
families = {'cedar': 'Pinaceae',
'oak': 'Fagaceae',
'beech': 'Fagaceae',
'camphor': 'Lauraceae',
'maple': 'Sapindaceae',
'phoebe': 'Lauraceae'
}
self.__family = families.get(self.__breed)
@property
def family(self): # 當然還得提供__family的getter。
'''The family property(getter).'''
return self.__family
@property
def breed(self):
'''The breed property(getter).'''
return self.__breed
@breed.setter # 本版順手補上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 # 本版順手補上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):
'''The height property(getter).'''
return self.__height
@height.setter # 本版順手補上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
# 不用寫那些boilerplates。
測試程式:
def show_breed_family(breed: str, family: str):
print(f'{breed=:12}{family=}')
tree1 = Tree('beech', 260, 307)
show_breed_family(tree1.breed, tree1.family)
tree2 = Tree('phoebe', 1_800, 56)
show_breed_family(tree2.breed, tree2.family)
tree3 = Tree('cedar', 3_560, 44)
show_breed_family(tree3.breed, tree3.family)
輸出:
__post_init()
大致如此使用。
註1: boilerplate code有人稱為「模板程式碼」。意思是一些有需要且不能省,但形式較為固定,內容不會改變或只需小幅度修改的code。
#include <iostream>
using namespace std;
int main()
{
return 0;
}
就是boilerplate code。if __name__ == '__main__':
...
也算是個boilerplate。