Python Data Classes

在 Python 3.7 中引入的 dataclasses 模組,提供了一個裝飾器 @dataclass,極大地簡化了主要用於儲存資料的類別定義。

為什麼需要 Data Classes?

在傳統類別中,定義一個簡單的資料容器需要寫很多重複的程式碼:

# 傳統寫法
class User:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email
    
    def __repr__(self):
        return f"User(name='{self.name}', age={self.age}, email='{self.email}')"
    
    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return (self.name, self.age, self.email) == (other.name, other.age, other.email)

使用 dataclass 可以自動產生 __init____repr____eq__ 等方法:

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str

# 自動產生 __init__
u1 = User("Alice", 25, "alice@example.com")
u2 = User("Alice", 25, "alice@example.com")

# 自動產生 __repr__
print(u1)  # User(name='Alice', age=25, email='alice@example.com')

# 自動產生 __eq__
print(u1 == u2)  # True

預設值

你可以像函數參數一樣給欄位指定預設值:

@dataclass
class Product:
    name: str
    price: float
    quantity: int = 1  # 預設值
    active: bool = True

p = Product("Apple", 20.0)
print(p)  # Product(name='Apple', price=20.0, quantity=1, active=True)

使用 field() 自訂欄位

對於複雜的預設值(如 list 或 dict),不能直接使用 = 賦值,否則所有實例會共享同一個物件(這是 Python 的陷阱)。應該使用 field(default_factory=...)

from dataclasses import dataclass, field
from typing import List

@dataclass
class Student:
    name: str
    grades: List[int] = field(default_factory=list)  # 正確做法

s1 = Student("Bob")
s1.grades.append(90)

s2 = Student("Charlie")
print(s2.grades)  # [],不會受到 s1 影響

field() 還可以控制欄位是否參與 __init____repr__ 等方法的生成:

@dataclass
class Account:
    username: str
    password: str = field(repr=False)  # 不顯示在 print 輸出中

acc = Account("alice", "secret123")
print(acc)  # Account(username='alice')

__post_init__ 處理

自動產生的 __init__ 執行完後,會呼叫 __post_init__ 方法。這適合用來做額外的初始化或驗證:

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # 不需要傳入,稍後計算

    def __post_init__(self):
        self.area = self.width * self.height

rect = Rectangle(5, 3)
print(rect.area)  # 15.0

不可變物件 (Frozen Instances)

設定 frozen=True 可以讓 Data Class 變成不可變的(類似 tuple):

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

p = Point(10, 20)
# p.x = 30  # FrozenInstanceError: cannot assign to field 'x'

這使得物件可以用作 dictionary 的 key 或加入 set 中。

轉換為字典或元組

dataclasses 提供了輔助函數將物件轉換為標準資料結構:

from dataclasses import asdict, astuple

@dataclass
class Color:
    r: int
    g: int
    b: int

c = Color(255, 0, 0)

print(asdict(c))   # {'r': 255, 'g': 0, 'b': 0}
print(astuple(c))  # (255, 0, 0)

繼承

Data Classes 支援繼承,子類別會包含父類別的欄位:

@dataclass
class Base:
    x: int

@dataclass
class Derived(Base):
    y: int

d = Derived(x=10, y=20)
print(d)  # Derived(x=10, y=20)