跳至主要內容

Python Dataclass 實用技巧全攻略

6 分鐘閱讀 1,000 字

Python Dataclass 實用技巧全攻略

Python 3.7 引入的 dataclasses 模組讓定義資料容器類別變得更加簡潔。不再需要手動撰寫 __init____repr____eq__ 等樣板程式碼。但 dataclass 的功能遠不止如此,本文將帶你深入探索各種實用技巧。

基礎回顧

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class User:
    name: str
    email: str
    age: int = 0
    tags: list[str] = field(default_factory=list)
    bio: Optional[str] = None

# 自動生成 __init__, __repr__, __eq__
user = User(name="Alice", email="alice@example.com", age=28)
print(user)  # User(name='Alice', email='alice@example.com', age=28, tags=[], bio=None)

注意:帶有預設值的欄位必須放在沒有預設值的欄位之後。可變型別(list、dict)必須用 field(default_factory=...) 而不是直接賦值。

技巧一:`__post_init__` 做資料驗證

from dataclasses import dataclass
from datetime import date

@dataclass
class Employee:
    name: str
    salary: float
    start_date: date

    def __post_init__(self):
        if self.salary < 0:
            raise ValueError(f"薪資不能為負數:{self.salary}")
        if self.start_date > date.today():
            raise ValueError("入職日期不能是未來")
        # 自動正規化
        self.name = self.name.strip().title()

emp = Employee(name=" bob smith ", salary=50000, start_date=date(2024, 1, 15))
print(emp.name)  # Bob Smith

技巧二:凍結不可變物件

@dataclass(frozen=True)
class Point:
    x: float
    y: float

    def distance_to(self, other: "Point") -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2))  # 5.0

# frozen=True 讓物件可以作為字典的 key 或放進 set
points = {p1, p2}

技巧三:`field` 的進階用法

from dataclasses import dataclass, field
from typing import ClassVar
import uuid

@dataclass
class Order:
    # ClassVar 不會被納入 __init__ 的參數
    _counter: ClassVar[int] = 0

    product: str
    quantity: int
    # repr=False:不出現在 __repr__ 輸出中
    internal_id: str = field(default_factory=lambda: str(uuid.uuid4()), repr=False)
    # compare=False:不參與 __eq__ 比較
    created_at: float = field(default_factory=lambda: __import__('time').time(), compare=False)
    # init=False:不能在建構子中傳入,只能由 __post_init__ 設定
    total_price: float = field(init=False)
    unit_price: float = 0.0

    def __post_init__(self):
        self.total_price = self.unit_price * self.quantity

技巧四:繼承與擴展

@dataclass
class Base:
    name: str
    created_at: str = field(default_factory=lambda: __import__('datetime').datetime.now().isoformat())

@dataclass
class Article(Base):
    title: str = ""
    body: str = ""
    is_published: bool = False

    def publish(self):
        self.is_published = True
        return self

article = Article(name="tech-blog", title="Hello World", body="...")
article.publish()

技巧五:與 `asdict` 和 `astuple` 互轉

from dataclasses import dataclass, asdict, astuple

@dataclass
class Config:
    host: str
    port: int
    debug: bool = False

config = Config(host="localhost", port=8080)

# 轉成字典(常用於 JSON 序列化)
config_dict = asdict(config)
print(config_dict)  # {'host': 'localhost', 'port': 8080, 'debug': False}

import json
print(json.dumps(config_dict))  # 直接序列化

# 從字典還原
new_config = Config(**config_dict)

# 轉成 tuple
print(astuple(config))  # ('localhost', 8080, False)

技巧六:`replace` 做不可變更新

類似 Rust 的 struct update syntax,dataclasses.replace 讓你基於現有物件建立修改版本:

from dataclasses import replace

@dataclass(frozen=True)
class AppState:
    user_id: Optional[int] = None
    theme: str = "light"
    language: str = "zh-TW"
    sidebar_open: bool = True

state = AppState(user_id=42)

# 建立修改後的新狀態,不改動原始物件
new_state = replace(state, theme="dark", sidebar_open=False)

print(state.theme)      # light
print(new_state.theme)  # dark

技巧七:搭配 `__slots__` 節省記憶體

Python 3.10 起,dataclass 支援 slots=True,自動加上 __slots__ 以節省記憶體:

@dataclass(slots=True)
class Particle:
    x: float
    y: float
    z: float
    mass: float

# 大量建立時記憶體用量明顯更少
particles = [Particle(i, i*2, i*3, 1.0) for i in range(100_000)]

技巧八:自訂排序

@dataclass(order=True)
class Student:
    # order=True 會按欄位順序比較
    # 用 field(compare=False) 排除不想比較的欄位
    gpa: float
    name: str = field(compare=False)
    student_id: int = field(compare=False)

students = [
    Student(gpa=3.8, name="Alice", student_id=1),
    Student(gpa=3.5, name="Bob", student_id=2),
    Student(gpa=3.9, name="Charlie", student_id=3),
]

# 按 GPA 排序
print(sorted(students, reverse=True))

Dataclass vs Pydantic

特性 Dataclass Pydantic
執行時型別驗證
JSON 序列化 需手動 內建
效能 較快 稍慢
學習曲線
適合場景 純資料容器 API 模型、設定檔

小結

Dataclass 是 Python 內建的輕量資料容器解決方案。掌握 __post_init__frozenfield 的各種參數、replaceslots 等功能,足以應對大多數的資料建模需求。當需要執行時型別驗證或複雜序列化時,再考慮引入 Pydantic。

分享這篇文章