今天還是再深入一點介紹dataclass(註1)好了。
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
id
(int)和location
(str)兩個屬性。於是,類別只好大動干戈,所有boilerplates
都得修改,工程有點大,有人認為這種改動,幾乎等於「重構」(refactor):
class Tree():
'''為了聚焦本class僅實作getters,略去setters, deleters。'''
def __init__(self, id: int, 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
測試程式:
tree1 = Tree(1, 'cedar', 50, 200, 'X235.6')
print(f'\n{tree1}')
tree2 = Tree(2, 'beech', 260, 307, 'J103.0')
print(f'\n{tree2}')
輸出:boilerplates
,不是很繁瑣嗎?from dataclasses import dataclass
@dataclass
class Tree():
__id: int # 新增
__breed: str
__age: int
__height: int
__location: str # 新增
@property
def id(self) -> int:
'''The id property(getter).'''
return self.__id
@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
@property
def location(self) -> str:
'''The location property(getter).'''
return self.__location
# 這時不用寫那幾個boilerplates了。dataclass會自動幫我們產生。
測試程式同上。boilerplate code
的擴充性。因為使用了dataclass,就幾乎不必自己動手寫boilerplate code
,省去很多複製貼上再修修補補的重複且無聊苦工。dataclass decorator本身有一些參數以供調校,增加彈性。Python 3.10有下列參數(註2):
init
:為True(預設)時產生建構子__init__()
。筆者按:這個參數是否有點多餘?當然要產生呀?否則要dataclass何用?repr
:為True(預設)時產生__repr()
方法。如已有自行設計的__repr__()
,以自訂者為準,dataclass甘拜下風。eq
:為True(預設)時產生__eq__()
方法。如已有自行設計的__eq__()
,dataclass從之。order
:為True(預設False)時產生以下方法:__lt__()
, __le__()
, __gt__()
,及__ge__()
。如order
為True而eq
為False會觸發ValueError。另外,如有自訂的__lt__()
, __le__()
, __gt__()
, 或__ge__()
,則賞您一個TypeError。unsafe_hash
:為False(預設)時依eq
和frozen
如何設定來產生__hash__()
方法。詳細說明請直接看官網。frozen
:此冰凍參數為True(預設False)時,物件一旦建造,屬性就無法修改,變成immutable dataclass
,性質有點像tuple。match_args
:為True(預設)時產生__match_args__
tuple。kw_only
:為True(預設False)時所有屬性均標記為keyword-only
。詳見官網。slots
:為True(預設False)時產生__slots__
屬性。如有自訂__slots__
會拋出TypeError。驗證。注意decorator有加參數,先看frozen=True
的效果:
from dataclasses import dataclass
@dataclass(frozen=True)
class Tree():
__id: int # 新增
__breed: str
__age: int
__height: int
__location: str # 新增
@property
def id(self) -> int:
'''The id property(getter).'''
return self.__id
@property
def breed(self) -> str:
'''The breed property(getter).'''
return self.__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) -> int:
'''The height property(getter).'''
return self.__height
@property
def location(self) -> str:
'''The location property(getter).'''
return self.__location
主程式:
try:
tree = Tree(1, 'cedar', 50, 200, 'X235.6')
print(f'\nbefore assignment: {tree.age = :<10,}')
tree.age = 100
except Exception as e:
print(str(e))
finally:
print(f'\nafter assignment : {tree.age = :<10,}')
輸出(屬性遭凍結無法修改):
再將frozen
參數改回False:
from dataclasses import dataclass
@dataclass(order=True, frozen=False)
class Tree():
__id: int # 新增
__breed: str
__age: int
__height: int
__location: str # 新增
@property
def id(self) -> int:
'''The id property(getter).'''
return self.__id
@property
def breed(self) -> str:
'''The breed property(getter).'''
return self.__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) -> int:
'''The height property(getter).'''
return self.__height
@property
def location(self) -> str:
'''The location property(getter).'''
return self.__location
主程式不變,這回修改年齡成功,表示屬性不是immutable:
Dataclass decorator另外提供field()方法。這裡dataclass的用詞是field
欄位,指的其實就是屬性attribute。為了和系列其他文章用詞一致,筆者還是用屬性而不用欄位。
field()方法功用是針對每一個屬性作個別設定,例如設定某屬性的預設值。請注意,預設值設定依然得遵守Python函數參數傳遞的老規距:先位置型參數後關鍵字參數:
from dataclasses import dataclass, field
@dataclass
class Tree():
__id: int
__age: int
__height: int
__location: str = field(default='X203.9') # similar to __location: str = 'X203.9'
__breed: str = field(default='cedar') # similar to __breed: str = 'cedar'
@property
def id(self) -> int:
'''The id property(getter).'''
return self.__id
@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
@property
def location(self) -> str:
'''The location property(getter).'''
return self.__location
主程式:
tree = Tree(1, 260, 307) # 只給3個位置型參數。
print(f'\n{tree}\n')
輸出:
Python 3.10版的dataclass,field()方法的所有參數如下,有些參數的意義筆者不明所以,只得原文照抄:
default
:提供本屬性的預設值。default_factory
:如有設定本參數,it must be a zero-argument callable that will be called when a default value is needed for this field.(一時未能消化其意,只好直接貼上原文)。本參數不能和default
參數共存。init
:為True(預設)時建構子__init__()
納入本屬性。repr
:為True(預設)時__repr__()
方法包含本屬性。hash
:本參數可為bool或None。True時__hash__()
方法將包含本屬性。None(預設)時視compare
參數而定。compare
:為True(預設)時__eq__(), __gt__()
等方法將包含本屬性。metadata
:可以是一個mapping或者為None。本參數包裹(wrapped)於MappingProxyType()方法內,使得該屬性成為唯讀,或曝露於Field物件。這句話的真義筆者也不了解,原文是:This can be a mapping or None. None is treated as an empty dict. This value is wrapped in MappingProxyType() to make it read-only, and exposed on the Field object.kw_only
:為True時本屬性會標記為keyword-only。小試field()方法:
from dataclasses import dataclass, field
@dataclass
class Tree():
__id: int
__age: int = field(init=False, repr=False)
__height: int = field(repr=False)
__location: str = field(default='X203.9') # similar to __location: str = 'X203.9'
__breed: str = field(default='cedar') # similar to __breed: str = 'cedar'
@property
def id(self) -> int:
'''The id property(getter).'''
return self.__id
@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
@property
def location(self) -> str:
'''The location property(getter).'''
return self.__location
測試程式:
try:
tree = Tree(1, 307)
print(f'\n{tree}\n')
print(f'{tree.location=}')
print(f'{tree.age=}')
except Exception as e:
print(str(e))
輸出:
說明:
__age
屬性故意設為init=False, repl=False
,使得建構子不納入本屬性。筆者測試的結果是init=False
時repl
也須設為False,否則在企圖執行__repl__()
方法時會找不到'_Tree__age'
。__height
屬性設為repr=False
,結果是該屬性不會納入__repl__()
,亦即print(f'\n{tree}\n')
這行時,不會印出樹高。__location
和__breed
兩屬性給了預設值,上面已有交代,不再贅述。註1: 本篇時而僅說dataclass,時而用dataclass decorator,兩者意義並無差異。
註2:這些參數和field()的參數均參考自Python官網3.10.7版文件。