Python dataclass

Python dataclass(Python Dataclass)

這篇 Python dataclass 教學接續前兩篇:你已經知道一個像樣的類別要寫 __init____repr____eq__⋯⋯全是樣板。@dataclass 裝飾器讓 Python 幫你自動生成這些,特別適合「主要用來裝資料」的類別。

傳統寫法 vs dataclass

同樣的 Point 類別,兩種寫法對照:

# 傳統寫法:12 行
class PointOld:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"PointOld(x={self.x}, y={self.y})"

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
# dataclass:4 行,功能相同
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(3, 5)
p2 = Point(3, 5)
print(p1)            # 自動有 __repr__
print(p1 == p2)      # 自動有 __eq__(比內容)

執行輸出:

Point(x=3, y=5)
True

x: int 這種寫法是型別提示(下一篇詳細介紹),dataclass 靠它知道有哪些欄位。

預設值

from dataclasses import dataclass

@dataclass
class Student:
    name: str
    score: int = 0           # 預設值
    school: str = "未填寫"

s = Student("小明")
print(s)

s2 = Student("小華", score=95)
print(s2)

執行輸出:

Student(name='小明', score=0, school='未填寫')
Student(name='小華', score=95, school='未填寫')

規則跟函式參數一樣:有預設值的欄位要放在沒預設值的後面

可變預設值:用 default_factory

List 當預設值的老問題在 dataclass 一樣存在,而且 dataclass 直接禁止你犯錯:

from dataclasses import dataclass

@dataclass
class Team:
    name: str
    members: list = []      # 直接報錯!

執行輸出:

ValueError: mutable default <class 'list'> for field members is not allowed: use default_factory

正確寫法用 field(default_factory=list),每個物件各自拿到新的空 List:

from dataclasses import dataclass, field

@dataclass
class Team:
    name: str
    members: list = field(default_factory=list)

a = Team("A 隊")
b = Team("B 隊")
a.members.append("小明")
print(a.members, b.members)    # 各自獨立

執行輸出:

['小明'] []

frozen:不可變的資料類別

frozen=True 讓物件建立後不能修改(類似 Tuple 的精神),而且自動變成可雜湊、能放進 set:

from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    host: str
    port: int

c = Config("localhost", 8080)
print(c)

c.port = 9999    # 嘗試修改

執行輸出:

Config(host='localhost', port=8080)
dataclasses.FrozenInstanceError: cannot assign to field 'port'

常見錯誤

1. TypeError: non-default argument follows default argument

沒預設值的欄位寫在有預設值的後面。調整欄位順序:必填在前、選填在後。

2. ValueError: mutable default … use default_factory

如上節,List/dict 預設值要用 field(default_factory=list)

3. 忘記寫型別提示

@dataclass 只認「有型別註記的類別變數」,寫 x = 0 而不是 x: int = 0,這個欄位會被忽略,建構時也不能傳。

總結

@dataclass 自動生成 __init____repr____eq__;預設值規則同函式參數、可變預設值用 default_factoryfrozen=True 做不可變設定物件。資料導向的類別一律優先考慮 dataclass。它依賴的型別提示語法,下一篇完整介紹。

延伸閱讀