和其他 Python 設計的類別或函式一樣,基本上 Dataclass 就是給懶惰的程式設計師用的。
當你的類別主要是處理資料時,你可以用 Dataclass 來自動幫你產生程式碼,而不用自己撰寫。
首先複習上次的內容
以下內容引用自 Chris 兄熱心提供的中英對照 Jupyter Notebook:
__post_init__
顧名思義,__post_init__
是在 __init__
之後做的事。
就像 Prepay 表示事前預付,而 Postpay 表示事後支付。
先看老師示範的最簡範例:
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
def __post_init__(self):
print('__post_init__ called')
print(repr(self))
c = CircleD()
輸出:
__post_init__ called
CircleD(x=0, y=0, radius=1)
我們可以把我們想在 __init__
中做的事,寫在 __post_init__
中, Dataclass 會幫忙處理掉瑣碎的事。
補充:
__post_init__
是實體方法(instance method),可以存取任何實體欄位(instance field,由 Dataclass__init__
實體化的欄位)。
什麼情境適合使用 __post_init__
?
-
驗證屬性值
-
初始化與外部資源的連接
-
相依屬性的計算
Init-Only Variables
在上述 __post_init__
中,我們還可以傳入參數,這些參數稱為 初始化專用變數(Init-Only Variables)。
初始化專用變數 不會存入實體記憶體中,所以必須使用參數方式數傳入 __post_init__
。
初始化專用變數 有特別的宣告方式(InitVar),而且需依序宣告。
from dataclasses import dataclass, InitVar
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
translate_x: InitVar[int] = 0 # 比較:translate_x: int = 0
translate_y: InitVar[int] = 0 # 比較:translate_y: int = 0
def __post_init__(self, translate_x, translate_y):
print(f"Translating center by: \u0394x={translate_x}, \u0394y={translate_y}")
self.x += translate_x
self.y += translate_y
c = CircleD(0, 0, 1, -1, -2)
## 輸出:Translating center by: Δx=-1, Δy=-2
c
## 輸出:CircleD(x=-1, y=-2, radius=1)
如果要將 translate_x
和 translate_y
設定為 keyword-only arguments,我們可以使用上一則影片中的 KW_ONLY
。
from dataclasses import dataclass, InitVar, KW_ONLY
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
_: KW_ONLY
translate_x: InitVar[int] = 0
translate_y: InitVar[int] = 0
def __post_init__(self, translate_x, translate_y):
print(f"Translating center by: \u0394x={translate_x}, \u0394y={translate_y}")
self.x += translate_x
self.y += translate_y
try:
c = CircleD(0, 0, 1, -1, -2)
except TypeError as ex:
print(f"TypeError: {ex}")
## 輸出:TypeError: CircleD.__init__() takes from 1 to 4 positional arguments but 6 were given
try:
c = CircleD(0, 0, 1, translate_x=-2, translate_y=-1)
## 輸出:Translating center by: Δx=-2, Δy=-1
except TypeError as ex:
print(f"TypeError: {ex}")
c
## 輸出:CircleD(x=-2, y=-1, radius=1)
證明一下 translate_x 和 translate_y 不會存入 instance field 記憶體中。
c.__dict__
## 輸出:{'x': -2, 'y': -1, 'radius': 1}
# 或用 fields(CircleD) 也可以檢查
# from dataclasses import fields
# fields(CircleD)
Field Level Customizations
參數 | 描述 |
---|---|
default |
屬性的默認值。如果使用者未提供該屬性的值,將使用默認值。 |
default_factory |
可呼叫物件,用於在需要默認值時動態計算。 |
init |
布林值,表示是否將此屬性包含在 __init__ 方法中。 |
repr |
布林值,表示是否將此屬性包含在 __repr__ 方法中。 |
hash |
布林值,表示是否將此屬性包含在自動生成的 __hash__ 方法中。 |
compare |
布林值,表示是否將此屬性包含在自動生成的 __eq__ 和 __ne__ 方法中。 |
metadata |
字典,用於存儲自定義的屬性元數據。 |
kw_only |
布林值,表示是否將此屬性僅接受關鍵字引數提供。 |
重要提示: 必須使用
field()
函數來創建Field
類別的實體,而不要直接使用Field
來實體化。
以下是 Field
實體化範例:
from dataclasses import field, Field
field()
輸出
Field(
name=None,
type=None,
default=<dataclasses._MISSING_TYPE object at 0x7aedf3cb3d90>,
default_factory=<dataclasses._MISSING_TYPE object at 0x7aedf3cb3d90>,
init=True,
repr=True,
hash=None,
compare=True,
metadata=mappingproxy({}),
kw_only=<dataclasses._MISSING_TYPE object at 0x7aedf3cb3d90>,
_field_type=None
)
from dataclasses import field, Field
f = field()
type(f)
輸出
dataclasses.Field
field 函式的 repr
參數
使用 repr
參數,來指定要顯示(represent)的屬性內容。
原本的正常顯示是這樣(所有的屬性都顯示):
from dataclasses import dataclass, field, Field
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
c = CircleD()
c
## 輸出
CircleD(x=0, y=0, radius=1)
現在我們只想秀半徑(radius),而把座標 (x, y) 隱藏起來。
方法一:程式手工業
實作 __repr__
。
from dataclasses import dataclass, field, Field
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
def __repr__(self):
return f"{self.__class__.__qualname__}(radius={self.radius})"
c = CircleD()
c
## 輸出
CircleD(radius=1)
方法二:設定 field 的 repr 參數
設定 repr
參數,指定要顯示(represent)的屬性內容。
from dataclasses import dataclass, field, Field
@dataclass
class CircleD:
x: int = field(repr=False)
y: int = field(repr=False)
radius: int = 1
c = CircleD(0, 0, 1) ## <=====
c
## 輸出
CircleD(radius=1)
field 函式的 default
參數
前一段說明 repr
時,最後的例子裡,是用 c = CircleD(0, 0, 1)
來初始化,而不像前兩個例子使用 c = CircleD()
。
這是因為老師接下來就要介紹 default
參數。
from dataclasses import dataclass, field, Field
@dataclass
class CircleD:
x: int = field(default=0, repr=False)
y: int = field(default=0, repr=False)
radius: int = 1
c = CircleD() ## <=====
c
## 輸出
CircleD(radius=1)
Non-Initialized Fields
前面說明了如何創建(偽)欄位,而且這些欄位不是真正的欄位(dataclass 中的屬性),還能當作傳給 __init__
和 __post_init__
的參數。
這節講的剛好倒過來:在類別中定義一個欄位,而且不一定會當作傳給 __init__
的參數。
這個作法應該非常適合使用在計算欄位。
from dataclasses import dataclass
from math import pi
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
def __post_init__(self):
self._area = pi * self.radius ** 2
@property
def area(self):
return self._area
c = CircleD()
c.area
## 輸出
3.141592653589793
這種方法有幾個問題:
-
area
是一個常規屬性,而且不會顯示在資料類別的欄位中,這不是我們想要的。 -
類別狀態中,有一個額外的「backing 變數」
_area
。(「backing 變數」:封裝起來的屬性) -
如果 dataclass 被凍結,嘗試在
__post_init__
方法中存儲self._area
將會失敗!
c.__dict__
## 輸出
{'x': 0, 'y': 0, 'radius': 1, '_area': 3.141592653589793}
from dataclasses import fields
fields(c)
## 輸出(只舉第一個 name 作範例)
Field(
name='x',
type=<class 'int'>,
default=0,
default_factory=<dataclasses._MISSING_TYPE object at 0x7f3f3f1ebd90>,
init=True,
repr=True,
hash=None,
compare=True,
metadata=mappingproxy({}),
kw_only=False,
_field_type=_FIELD),
如果我們想保持簡捷一致,我們真正需要的是一個在類別中定義的欄位,但不是預期的 __init__
(和 __post_init__
)的參數。
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
area: float = field(init=False, repr=False) ## =====>
def __post_init__(self):
self.area = pi * self.radius ** 2
c = CircleD()
c.__dict__
## 輸出
{'x': 0, 'y': 0, 'radius': 1, 'area': 3.141592653589793}
c.area
## 輸出
3.141592653589793
fields(c)
## 輸出(省略)
如果我們想要,我們也可以選擇凍結類別。
但如果凍結類別,實體變數的任何賦值(即使在 __post__init__
內部)都不會生效。
所以會出現錯誤,如下:
@dataclass(frozen=True)
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
area: float = field(init=False, repr=False)
def __post_init__(self):
self.area = pi * self.radius ** 2
from dataclasses import FrozenInstanceError
try:
c = CircleD()
except FrozenInstanceError as ex:
print(f"FrozenInstanceError: {ex}")
## 輸出
FrozenInstanceError: cannot assign to field 'area' ## =====>
我們可以使用父類別(super())上的 __setattr__
,來繞過資料類別的凍結保護,但這將導致雜湊時可能出現問題。
我們在上一段影片中,看到了可變性和雜湊性可能會出現問題。
@dataclass(frozen=True)
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
area: float = field(init=False, repr=False)
def __post_init__(self):
super().__setattr__("area", pi * self.radius ** 2) ## =====>
## 以下的輸出測試同前,略過
我們甚至可以利用這一點來實現屬性的惰性評估(lazy evaluation):
@dataclass(frozen=True)
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
_area: float = field(init=False)
@property
def area(self):
if not getattr(self, '_area', None):
print("Cache miss")
super().__setattr__('_area', pi * self.radius ** 2)
else:
print("Cache hit")
return self._area
c = CircleD()
c.__dict__
## 輸出
{'x': 0, 'y': 0, 'radius': 1}
c.area
## 輸出
Cache miss ## =====>
3.141592653589793
c.area
## 輸出
Cache hit ## =====>
3.141592653589793
不建議這種解決方式(super().__setattr__()
)。
我們有時會理由,設計不包含在初始化引數中的欄位,但是當 dataclass 被凍結時,它就會衍生問題。
field 函式的 compare
參數
透過指定 field 函式的 compare
參數,我們可以改變 __eq__
的行為。(因為 dataclass 好心的幫我們實作了簡單的 __eq__
)
其實還有 __lt__
, __le__
。
相等和(默認)排序順序,是基於 dataclass 的所有欄位 (按順序) 。
老師舉的例子是:將原本座標、半徑都相同的圓,改為半徑相同即判定兩圓相等。
from dataclasses import dataclass, field, Field
@dataclass(order=True)
class CircleD:
x: int = field(default=0, compare=False)
y: int = field(default=0, compare=False)
radius: int = 1
c1 = CircleD(1, 0, 2)
c2 = CircleD(1, 1, 1)
c1 <= c2
## 輸出
False
c3 = CircleD(1, 0, 1)
c4 = CircleD(1, 1, 1)
c3 = c4
## 輸出
True
提醒:使用 dataclass 是因為方便,它幫我們實作了許多功能。所以如果你要做的事,違背了一般常理,你要實作(修改 dataclass 幫你做)的部分會愈來愈多,那不如直接使用一般的類別就好。
Hashing Partially Mutable Classes
用於為實體創建雜湊的實體狀態,應該是不可變的(但這並不表示整個類別都是不可變的)。
在大多數情況下,我們應該遵循這些規則:
-
用於為實體生成雜湊的實體資料,應該是不可變的
-
用於生成雜湊的相同資料,是實作相等性的一部分
-
兩個相等的實體,應該有相同的雜湊
首先我們來看看之前的例子,符合上面三條件的二和三。
class Person:
def __init__(self, name, age, ssn):
self.name: str = name # this could change over time
self.age: int = age # this changes over time
self.ssn: str = ssn # this never changes over time
def __eq__(self, other):
if self.__class__ == other.__class__:
return self.ssn == other.ssn
return NotImplemented
def __hash__(self):
return hash(self.ssn)
接著修改程式,來符合第一要件。
class Person:
def __init__(self, name, age, ssn):
self.name: str = name # this could change over time
self.age: int = age # this changes over time
self._ssn: str = ssn # this never changes over time
def __eq__(self, other):
if self.__class__ == other.__class__:
return self.ssn == other.ssn
return NotImplemented
def __hash__(self):
return hash(self.ssn)
@property
def ssn(self):
return self._ssn
def __repr__(self):
return f"Person(name={self.name}, age={self.age}, ssn={self.ssn}, id={hex(id(self))})"
驗證是否符合(一)
p1 = Person('A', 30, '12345')
p2 = Person('B', 40, '23456')
p3 = Person('C', 50, '12345')
p1 == p2, p1 == p3
## 輸出
(False, True)
hash(p1), hash(p2), hash(p3)
## 輸出
(-5618707572131111372, 441038772423436139, -5618707572131111372)
驗證是否符合(二)
以 set & dict 驗證
{p1, p2, p3}
## 輸出
{Person(name=A, age=30, ssn=12345, id=0x7f7d7fafb4c0),
Person(name=B, age=40, ssn=23456, id=0x7f7d7faf96f0)}
d = {p1: "Person 1", p2: "Person 2"}
d
## 輸出
{Person(name=A, age=30, ssn=12345, id=0x7f7d7fafb4c0): 'Person 1',
Person(name=B, age=40, ssn=23456, id=0x7f7d7faf96f0): 'Person 2'}
只要不改到 ssn(處理 hash),其他的 name & age 怎麼改都沒關係。
p1.name = 'X'
p1.age=100
d
## 輸出
{Person(name=X, age=100, ssn=12345, id=0x7f7d7fafb4c0): 'Person 1',
Person(name=B, age=40, ssn=23456, id=0x7f7d7faf96f0): 'Person 2'}
d[p1]
## 輸出
Person 1
當我們將 dataclass 設為 frozen=True
時,會自動幫我們處理雜湊的相關事宜(別忘了 dataclass 號稱程式產生器。frozen 就成為不可變,也就是可雜湊)。
@dataclass(frozen=True)
class Person:
name: str
age: int
ssn: str
p1 = Person('A', 30, '12345')
p2 = Person('B', 40, '23456')
{p1, p2} ## 實例可雜湊
## 輸出
{Person(name='A', age=30, ssn='12345'), Person(name='B', age=40, ssn='23456')}
p1 = Person('A', 30, '12345')
p2 = Person('A', 30, '12345')
p3 = Person('B', 40, '12345')
p1==p2, p2==p3, p1==p3
## 輸出
(True, False, False)
到目前為止,兩個實例相等,是指所有欄位相等。
不過我們只想讓 ssn 不可變,讓 name & age 是可變的。
@dataclass(frozen=True)
class Person:
name: str = field(compare=False)
age: int = field(compare=False)
ssn: str
p1 = Person('A', 30, '12345')
p2 = Person('B', 40, '12345')
p1 is p2
## 輸出:False
p1 == p2
## 輸出:True
hash(p1) == hash(p2)
## 輸出:True
如果我們使用 order=True
裝飾器使 dataclass 可排序,當然會影響默認排序。
所以即使從技術上可以改變 name
和 age
屬性,我們也不能這樣做:
from dataclasses import FrozenInstanceError
try:
p1.name = 'X'
except FrozenInstanceError as ex:
print(f"FrozenInstanceError: {ex}")
## 輸出
FrozenInstanceError: cannot assign to field 'name'
field 函式的 unsafe_hash
參數
上面的問題要如何解決呢?
我們前面才剛看過老師示範用 super().__setattr__()
來解決,但這不是好方法。
假設我們可以讓 dataclass 設回可改變,而且所有開發人員都知道不可修改 不可變
的屬性:
from dataclasses import dataclass, field, Field
@dataclass(unsafe_hash=True) ## =====>
class Person:
name: str = field(compare=False)
age: int = field(compare=False)
ssn: str
p1 = Person('A', 30, '12345')
p2 = Person('B', 40, '12345')
p1 is p2, p1 == p2, hash(p1) == hash(p2)
## 輸出
(False, True, True)
而且我們可以修改 name & age。
p1 = Person('A', 30, '12345')
p2 = Person('B', 40, '23345')
d = {
p1: 'Person A',
p2: 'Person B'
}
d
## 輸出
{Person(name='A', age=30, ssn='12345'): 'Person A',
Person(name='B', age=40, ssn='23345'): 'Person B'}
p1.name = 'AAA'
p1.age = 300
d
## 輸出
{Person(name='AAA', age=300, ssn='12345'): 'Person A',
Person(name='B', age=40, ssn='23345'): 'Person B'}
但前面已強調「所有開發人員都知道不可修改 不可變
的屬性」,因為其實可以修改 ssn。
p2.ssn = '12345'
d
## 輸出
{Person(name='AAA', age=300, ssn='12345'): 'Person A',
Person(name='B', age=40, ssn='12345'): 'Person A'}
d[p1]
## 輸出
Person A
d[p2]
## 輸出
Person A
果然出槌了!
field 函式的 kw_only
參數
前面已提及使用 KW_ONLY
關鍵字來區分 Keyword-Only Arguments。範例:
from dataclasses import dataclass, field, Field, InitVar, KW_ONLY
@dataclass
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
_: KW_ONLY
translate_x: InitVar[int] = 0
translate_y: InitVar[int] = 0
def __post_init__(self, translate_x, translate_y):
print(f"Translating center by: \u0394x={translate_x}, \u0394y={translate_y}")
self.x += translate_x
self.y += translate_y
CircleD(0, 0, 1, translate_y=-1, translate_x=0)
## 輸出:
Translating center by: Δx=0, Δy=-1
CircleD(x=0, y=-1, radius=1)
我們知道 KW_ONLY
對傳入參數的限制:
try:
CircleD(0, 0, 1, 0, -1)
except TypeError as ex:
print(f"TypeError: {ex}")
## 輸出:
TypeError: CircleD.__init__() takes from 1 to 4 positional arguments but 6 were given
我們也知道 Python 的 keyword-only arguments,要放在 positional arguments 之後。
不過 dataclass 在 compile 時,會自己修正不符規定的順序。例如:
from dataclasses import dataclass, field, Field, InitVar, KW_ONLY
@dataclass
class CircleD:
x: int = 0
translate_x: InitVar[int] = field(default=0, kw_only=True) ## <=====
y: int = 0
translate_y: InitVar[int] = field(default=0, kw_only=True) ## <=====
radius: int = 1
def __post_init__(self, translate_x, translate_y):
print(f"Translating center by: \u0394x={translate_x}, \u0394y={translate_y}")
self.x += translate_x
self.y += translate_y
CircleD.__init__
## 輸出
<function __main__.CircleD.__init__(
self,
x: int = 0,
y: int = 0,
radius: int = 1,
*,
translate_x: dataclasses.InitVar[int] = 0,
translate_y: dataclasses.InitVar[int] = 0)
-> None>
我們還是可以依序創建 CircleD:
CircleD(0, 0, 1, translate_y=-1, translate_x=0)
## 輸出:
Translating center by: Δx=0, Δy=-1
CircleD(x=0, y=-1, radius=1)
以程式呼叫的方式建立 dataclass
因不確定觀看影片對象的程度,老師先複習了 named tuples。
方法一
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int
ssn: str
方法二
from collections import namedtuple
Person = namedtuple("Person", "name age ssn")
原本的 dataclass 是這樣:
from dataclasses import dataclass, field, Field, InitVar
@dataclass(order=True)
class CircleD:
x: int = 0
y: int = 0
radius: int = 1
translate_x: InitVar[int] = field(default=0, kw_only=True)
translate_y: InitVar[int] = field(default=0, kw_only=True)
def __post_init__(self, translate_x, translate_y):
print(f"Translating center by: \u0394x={translate_x}, \u0394y={translate_y}")
self.x += translate_x
self.y += translate_y
加上 make_dataclass
之後:
from dataclasses import dataclass, field, Field, InitVar, make_dataclass
def post_init(self, translate_x, translate_y):
print(f"Translating center by: \u0394x={translate_x}, \u0394y={translate_y}")
self.x += translate_x
self.y += translate_y
CircleD2 = make_dataclass(
'CircleD2',
[
('x', int, 0),
('y', int, 0),
('radius', int, 0),
('translate_x', InitVar[int], field(default=0, kw_only=True)),
('translate_y', InitVar[int], field(default=0, kw_only=True))
],
order=True,
namespace = {
"__post_init__": post_init
}
)
c = CircleD2(1, 2, 3, translate_x=1, translate_y=2)
## 輸出
Translating center by: Δx=1, Δy=2
c
## 輸出
CircleD2(x=2, y=4, radius=3)
field 函式的 metadata
參數
Python 目前沒有內建,但第三方函式庫已經有了。
在 dataclass 的 field 自訂 metadata 的方式,就是建立 mapping 對應的表和列。
最簡單直覺的作法,就是利用 mappingproxy。
建立 metadata 前
from dataclasses import dataclass, field, Field
@dataclass(unsafe_hash=True)
class Person:
name: str = field(compare=False)
age: int = field(compare=False)
ssn: str
建立 metadata 後
from dataclasses import dataclass, field, Field, fields ## <=====
@dataclass(unsafe_hash=True)
class Person:
name: str = field(compare=False, metadata={'table': 'person', 'column': 'name'})
age: int = field(compare=False, metadata={'table': 'person', 'column': 'current_age'})
ssn: str = field(metadata={'table': 'person', 'column': 'ssn'})
以 help 觀察,並沒有 metadata 的相關資訊(呼應前述 Python 未內建的敘述)。
請點擊展開觀看 help 文字說明:
Help on class Person in module __main__:
class Person(builtins.object)
| Person(name: str, age: int, ssn: str) -> None
|
| Person(name: str, age: int, ssn: str)
|
| Methods defined here:
|
| __eq__(self, other)
| Return self==value.
|
| __hash__(self)
| Return hash(self).
|
| __init__(self, name: str, age: int, ssn: str) -> None
| Initialize self. See help(type(self)) for accurate signature.
|
| __repr__(self)
| Return repr(self).
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| __annotations__ = {'age': <class 'int'>, 'name': <class 'str'>, 'ssn':...
|
| __dataclass_fields__ = {'age': Field(name='age',type=<class 'int'>,def...
|
| __dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,or...
|
| __match_args__ = ('name', 'age', 'ssn')
fields(Person)
## 輸出(僅取 name, age, ssn 中的 name 當範例)
(Field(
name='name',
type=<class 'str'>,
default=<dataclasses._MISSING_TYPE object at 0x7f7da0ec3d90>,
default_factory=<dataclasses._MISSING_TYPE object at 0x7f7da0ec3d90>,
init=True,
repr=True,
hash=None,
compare=False,
metadata=mappingproxy({'table': 'person', 'column': 'name'}), ## <=====
kw_only=False,
_field_type=_FIELD)
(fields(Person)[0]).metadata
## 輸出
mappingproxy({'table': 'person', 'column': 'name'})
field 函式的 default_factory
參數
首先以初學者常犯的錯誤做引言,這部分大家應該很熟,請自行點擊展開錯誤和正確程式碼的比對。
在函數中初始化的常見錯誤。請點擊展開
錯誤示範
def squares(i, l = []):
l.append((i, i ** 2))
return l
numbers = squares(1)
numbers
## 輸出
[(1, 1)]
others = squares(2)
others
## 輸出
[(1, 1), (2, 4)]
others = squares(3)
others
## 輸出
[(1, 1), (2, 4), (3, 9)]
說明:這是因為函數 squares 在編譯時(compile,不是呼叫 call,只發生一次),預設值
l
就已經創建(l = []
),並儲存在函數物件的狀態中。所以每次呼叫(call)函數時,都是存取 相同的 list,因此內容一直 append 進 list。
正確做法
def squares(i, l=None):
if l is None:
l = []
l.append((i, i ** 2))
return l
## 以下程式碼相同,輸出正確,省略
前面示範了函數的做法,接著以 class 示範同樣的問題。這部分大家還是很熟,請自行點擊展開錯誤和正確程式碼的比對。
在 class 中初始化的常見錯誤。請點擊展開
錯誤示範一
class Test:
def __init__(self, tests=[]):
self.tests = tests
def add(self, i):
self.tests.append((i, i ** 2))
t1 = Test()
t1.add(1)
t1.tests
## 輸出:
[(1, 1)]
t2 = Test()
t2.add(2)
t2.tests
## 輸出:
[(1, 1), (2, 4)]
錯誤示範二
class Test:
tests = []
def add(self, i):
self.tests.append((i, i ** 2))
## 以下程式碼相同,輸出一樣錯誤,省略
正確做法
在
__init__
中判斷 list 是否存在,若不存在就創建
class Test:
def __init__(self, tests=None):
if tests is None:
self.tests = []
else:
self.tests = tests
def add(self, i):
self.tests.append((i, i ** 2))
## 以下程式碼相同,輸出正確,省略
前面複習了 函數 和 類別 的做法,接下來看 dataclass 的做法,就很熟悉了。
錯誤示範
from dataclasses import dataclass
try:
@dataclass
class Test:
tests: list = []
def add(self, i):
self.tests.append((i, i ** 2))
except ValueError as ex:
print(f"ValueError: {ex}")
## 輸出
ValueError: mutable default <class 'list'> for field tests is not allowed: use default_factory
老師提醒我們,Python 會以是否可雜湊,來幫忙判斷,所以上述程式在 complie 階段就報錯。但我們必須心理有底,這種方法並不保證正確。
正確做法一
class Test:
def __init__(self, tests_factory):
self.tests = tests_factory()
def add(self, i):
self.tests.append((i, i ** 2))
def factory_func():
return []
t1 = Test(factory_func)
t1.add(1)
t1.tests
## 輸出
[(1, 1)]
## 輸出
t2 = Test(factory_func)
t2.add(2)
t2.tests
[(2, 4)]
接著簡化上面的程式:直接以 list
取代 factory_func
。(程式省略)
dataclass 的正確做法
from dataclasses import dataclass, field, Field
@dataclass
class Test:
# tests: list = field(default_factory=factory_func)
tests: list = field(default_factory=list)
def add(self, i):
self.tests.append((i, i ** 2))
t1 = Test()
t1.add(1)
t1.tests
## 輸出
[(1, 1)]
## 輸出
t2 = Test()
t2.add(2)
t2.tests
[(2, 4)]
t1 is t2
## 輸出
False
結論
補充資訊
為什麼要用 dataclass
這段影片很短,不到 9分鐘,卻以實際範例,精簡的說明了 dataclass 幫你做了哪些事(果然是懶人福音)。
順便送你個 bonus - attrs
。(僅提及,另有專門影片介紹)
看看 dataclass 幫你自動產生的 code 有哪些?
程式來源:上述影片 James Murphy 的 github。
import inspect
from dataclasses import dataclass, field
from pprint import pprint
@dataclass(frozen=True, order=True)
class Comment:
id: int
text: str
def main():
comment = Comment(1, "I just subscribed!")
print(comment)
pprint(inspect.getmembers(Comment, inspect.isfunction)) ## <=====
if __name__ == '__main__':
main()
輸出:
Comment(id=1, text='I just subscribed!')
[('__delattr__', <function Comment.__delattr__ at 0x7a6f30be6d40>),
('__eq__', <function Comment.__eq__ at 0x7a6f30be70a0>),
('__ge__', <function Comment.__ge__ at 0x7a6f30be6e60>),
('__gt__', <function Comment.__gt__ at 0x7a6f30be6ef0>),
('__hash__', <function Comment.__hash__ at 0x7a6f30be6c20>),
('__init__', <function Comment.__init__ at 0x7a6f30be7250>),
('__le__', <function Comment.__le__ at 0x7a6f30be6f80>),
('__lt__', <function Comment.__lt__ at 0x7a6f30be7010>),
('__repr__', <function Comment.__repr__ at 0x7a6f30be72e0>),
('__setattr__', <function Comment.__setattr__ at 0x7a6f30be6dd0>)]