第 29 課 · 進階 · 19 分鐘

Python dataclasses

用聲明式欄位定義以資料為中心的類,減少重複樣板。

dataclasses欄位預設值asdict

訂單是一條小記錄:編號、明細、總價、是否付款。手寫 __init__、__repr__、__eq__ 就是整整一頁樣板。@dataclass 免費生成了什麼?

dataclass 是帶有自動生成方法的普通類,初始化、顯示和相等比較都不必手寫。它適合點、使用者、設定值這類小型記錄。欄位通過註解聲明,預設值則像普通賦值一樣寫在後面。 你還會用 default_factory、在 __post_init__ 中驗證,並用 replace 更新凍結記錄。

用註解聲明欄位

@dataclass 裝飾器讀取帶註解的類屬性,並自動生成 __init__。建立 Point(2, 3) 會按順序給 x 和 y 賦值。自動生成的 __repr__ 讓物件在控制臺裡更易讀。

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

point = Point(2, 3)
print(point)

一個小型記錄

添加預設值

帶預設值的欄位可以在構造時省略。沒有預設值的欄位必須放在類體前面。repr 會同時顯示欄位名和目前值。

from dataclasses import dataclass

@dataclass
class User:
    name: str
    active: bool = True

print(User("Ada"))

欄位列表中的預設值

比較與轉換

dataclass 會根據欄位生成 __eq__,所以資料相同的兩個物件相等。asdict 回傳普通字典,便於 JSON 或測試斷言。frozen=True 會禁止之後再給屬性賦值。

from dataclasses import dataclass, asdict

@dataclass(frozen=True)
class Item:
    name: str
    price: float

item = Item("tea", 3.5)
print(asdict(item))
print(Item("tea", 3.5) == item)

相等比較與普通映射

可變預設值需要工廠

dataclass 裡的列表預設值會被所有實例共用,所以 Python 會直接拒絕。field(default_factory=list) 會為每個物件建立新列表。字典、集合和其他可變值同理。

from dataclasses import dataclass, field

@dataclass
class Order:
    drink: str
    tags: list[str] = field(default_factory=list)

first = Order("latte")
second = Order("tea")
first.tags.append("gift")
print(first.tags)
print(second.tags)
print(first.tags is second.tags)

每個實例都有自己的新列表

__post_init__ 驗證並派生

__post_init__ 在自動生成的 __init__ 之後執行。可以用它拒絕不可能的值,並計算必須保持一致的欄位。它應該驗證業務規則,而不是試圖強制執行 Python 根本沒檢查過的型別提示。

from dataclasses import dataclass

@dataclass
class Order:
    price: float
    quantity: int
    total: float = 0.0

    def __post_init__(self):
        if self.price < 0 or self.quantity <= 0:
            raise ValueError("invalid order")
        self.total = round(self.price * self.quantity, 2)

for values in [(4.5, 2), (-1, 1)]:
    try:
        print(Order(*values))
    except ValueError as error:
        print(type(error).__name__, error)

先驗證,再派生總價

用 replace 更新凍結記錄

frozen=True 禁止屬性賦值,讓記錄可以安全共用。replace() 回傳一個新實例,只改動指定欄位,原物件保持不變。這個模式適合撤銷、歷史記錄和設定快照。

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Order:
    drink: str
    price: float

original = Order("latte", 4.5)
updated = replace(original, price=5.0)
print(original)
print(updated)
print(original == updated)

複製後修改,而不是原地改

本課要點

  • @dataclass 會生成 __init__、__repr__ 和 __eq__。
  • 欄位是帶註解的類屬性。
  • 帶預設值的欄位必須放在沒有預設值的欄位之後。
  • asdict 會把 dataclass 實例轉換成字典。
  • frozen=True 會讓實例不可變。
  • 可變欄位用 default_factory;__post_init__ 負責驗證;凍結記錄用 replace 更新。

常見錯誤

  • 把沒有預設值的欄位放在有預設值的欄位之後。
  • 以為 dataclass 欄位會在執行時驗證型別。
  • 試圖給 frozen 實例賦值。
  • 寫 tags: list[str] = [],讓所有實例共用同一個列表。
輪到你

建立一個帶 name 和 price 的 frozen Item dataclass,再列印 asdict(Item('tea', 3.5))。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← 上下文管理器itertools →