本文 AI 產出,尚未審核

Python 物件導向程式設計(OOP)─ 資料類別(@dataclass)

簡介

在 Python 3.7 之後,@dataclass 成為標準函式庫的一部分,讓 資料類別(Data Class)可以用極少的程式碼描述「只負責保存資料」的物件。對於需要大量 DTO(Data Transfer Object)或簡單模型的專案,使用 dataclass 能大幅減少樣板程式(boilerplate),提升可讀性與維護性。

對於剛踏入物件導向的學習者,了解 @dataclass 不僅能快速建立符合慣例的類別,還能深入掌握 Python 內建的 型別註記(type hint)與 自動生成的特殊方法(如 __init____repr____eq__),這些概念在日後編寫更複雜的系統時都非常有幫助。

本篇文章將從核心概念說明開始,搭配多個實作範例,最後探討常見陷阱、最佳實踐與實務應用,幫助讀者在 初學者到中級開發者 的階段,能夠自信地在專案中使用 @dataclass


核心概念

1. 為什麼需要資料類別?

在傳統的 Python 類別中,我們必須手動撰寫 __init____repr____eq__ 等方法,程式碼往往長得像這樣:

class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point(x={self.x}, y={self.y})'

    def __eq__(self, other):
        return isinstance(other, Point) and self.x == other.x and self.y == other.y

使用 @dataclass 後,以上三個方法會 自動生成,只需要寫出屬性的型別註記:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

如此一來,程式碼更簡潔、錯誤率更低,也更符合「資料類別只負責保存資料」的語意。


2. 基本語法與常用參數

參數 功能說明 預設值
init 是否自動產生 __init__ 方法 True
repr 是否自動產生 __repr__ 方法 True
eq 是否自動產生 __eq__ 方法 True
order 是否產生排序相關方法 (__lt____le____gt____ge__) False
frozen 設為 True 時,物件變為不可變(類似 namedtuple False
slots 使用 __slots__ 減少記憶體開銷(Python 3.10+) False

範例:建立一個不可變且支援排序的資料類別:

from dataclasses import dataclass

@dataclass(order=True, frozen=True)
class Employee:
    id: int
    name: str
    salary: float

3. 預設值與 field()

若屬性需要預設值或更進階的設定(如排除於 repr、自訂比較),可以使用 dataclasses.field

from dataclasses import dataclass, field
from typing import List

@dataclass
class TodoList:
    title: str
    items: List[str] = field(default_factory=list)   # 使用 factory 建立獨立的 list
    completed: bool = field(default=False, repr=False)  # 不顯示於 __repr__
  • default_factory:避免所有實例共享同一個可變物件(如 list、dict)。
  • repr=False:在 __repr__ 中隱藏此欄位,保護敏感資訊。

4. __post_init__:自訂初始化後的處理

有時候需要在自動產生的 __init__ 之後執行額外的驗證或轉換,__post_init__ 提供了這個機會:

from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)   # 不由 __init__ 參數提供

    def __post_init__(self):
        if self.width <= 0 or self.height <= 0:
            raise ValueError('寬度與高度必須為正數')
        self.area = self.width * self.height   # 自動計算面積

__post_init__ 會在自動生成的 __init__ 完成後被呼叫,允許我們執行驗證或衍生欄位的計算。


5. 繼承與 @dataclass

資料類別同樣支援繼承,只要在子類別再次使用 @dataclass 即可:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

@dataclass
class Student(Person):
    school: str
    grade: int

子類別會自動繼承父類別的欄位與自動生成的方法,除非顯式覆寫。


程式碼範例

範例 1:簡易的座標點與向量運算

from dataclasses import dataclass

@dataclass
class Vector2D:
    x: float
    y: float

    def __add__(self, other: 'Vector2D') -> 'Vector2D':
        """向量相加"""
        return Vector2D(self.x + other.x, self.y + other.y)

    def magnitude(self) -> float:
        """計算向量長度"""
        return (self.x ** 2 + self.y ** 2) ** 0.5

# 使用範例
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1 + v2)          # Vector2D(x=4, y=6)
print(v1.magnitude())  # 5.0

說明@dataclass 為我們自動生成 __init____repr__,只需專注於向量的業務邏輯。


範例 2:不可變的設定物件(frozen=True

from dataclasses import dataclass

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

# 建立設定
cfg = Config(host='localhost', port=8080, debug=True)

# 嘗試修改會拋出例外
# cfg.port = 9090   # -> dataclasses.FrozenInstanceError
print(cfg)

重點frozen=True 讓物件變成不可變,適合存放全域設定或作為快取鍵值。


範例 3:使用 field(default_factory=…) 防止可變物件共享

from dataclasses import dataclass, field
from typing import Dict

@dataclass
class Counter:
    counts: Dict[str, int] = field(default_factory=dict)

    def inc(self, key: str, amount: int = 1) -> None:
        self.counts[key] = self.counts.get(key, 0) + amount

c1 = Counter()
c2 = Counter()
c1.inc('apple')
c2.inc('banana')
print(c1.counts)  # {'apple': 1}
print(c2.counts)  # {'banana': 1}

說明:若直接寫 counts: Dict[str, int] = {}c1c2 會共享同一個字典,導致資料互相污染。default_factory 為每個實例建立獨立的容器。


範例 4:自訂 __post_init__ 進行資料驗證

from dataclasses import dataclass, field

@dataclass
class User:
    username: str
    email: str
    age: int = field(default=0)

    def __post_init__(self):
        if '@' not in self.email:
            raise ValueError('Email 必須包含 @ 符號')
        if self.age < 0:
            raise ValueError('年齡不能為負數')

# 正確建立
u = User('alice', 'alice@example.com', 25)

# 觸發驗證錯誤
# User('bob', 'invalid-email')  # -> ValueError

範例 5:排序與比較(order=True

from dataclasses import dataclass

@dataclass(order=True)
class Task:
    priority: int
    name: str

tasks = [Task(3, '寫文件'), Task(1, '寫程式'), Task(2, '測試')]
tasks.sort()  # 依 priority 升冪排序
print(tasks)  # [Task(priority=1, name='寫程式'), Task(priority=2, name='測試'), Task(priority=3, name='寫文件')]

技巧order=True 會根據欄位的宣告順序自動產生比較方法,對於排程、優先佇列等情境相當便利。


常見陷阱與最佳實踐

陷阱 可能的後果 解決方案
可變預設值直接寫在欄位上 (list=[]) 所有實例共享同一個物件,導致資料互相污染 使用 field(default_factory=…)
忘記 frozen=True 造成意外修改 在多執行緒或快取情境下資料被改寫,產生難以追蹤的錯誤 若物件應該是「只讀」就加上 frozen=True
__repr__ 暴露敏感資訊 日誌或除錯輸出時洩漏密碼、金鑰等 field(..., repr=False) 隱藏欄位
過度使用 order=True 產生不必要的比較方法,增加維護成本 只在真的需要排序時才開啟
繼承時忘記在子類別加上 @dataclass 父類別的自動方法不會傳遞到子類別,必須手動實作 子類別必須同樣加上 @dataclass(或使用 @dataclass(eq=False) 只繼承部分功能)

最佳實踐

  1. 盡量使用型別註記dataclass 的核心在於型別資訊,能讓 IDE、型別檢查工具(mypy)發揮功效。
  2. 預設值使用 default_factory:特別是可變容器(list、dict、set)或自訂類別。
  3. 將不可變的資料類別設為 frozen=True:有助於安全的快取鍵、集合元素。
  4. 在需要自訂驗證時使用 __post_init__,避免在 __init__ 中寫過長的程式碼。
  5. 保持欄位宣告順序與業務重要度一致:排序比較會依此順序進行,讓程式行為更直觀。

實際應用場景

場景 為何適合使用 @dataclass
API 輸入/輸出模型 可以快速定義 JSON 反序列化/序列化的資料結構,配合 pydanticmarshmallow 使用。
遊戲開發的實體(Entity) 如座標、速度、生命值等屬性,使用 frozen=False 讓狀態可變,且自動產生 __repr__ 方便除錯。
金融交易的不可變記錄 交易編號、時間、金額等欄位設為 frozen=True,保證交易資料在程式運行期間不會被意外修改。
排程系統的任務描述 order=True 可直接以優先權排序任務列表,省去自行實作比較邏輯。
設定檔或環境變數的封裝 把環境變數映射成資料類別,使用 frozen=True 防止程式中途改變設定值。

總結

@dataclass 為 Python 提供了一套 簡潔、功能完整 的資料類別實作方式。透過自動生成的 __init____repr____eq__,以及可選的 orderfrozenslots 等參數,我們可以在 數行程式碼 內建立安全、可讀、易維護的模型。

在實務開發中,從 API 請求模型、遊戲實體、金融交易記錄,到排程任務與全域設定,dataclass 都能大幅減少樣板程式,讓開發者把精力集中在 業務邏輯 上。只要留意可變預設值、敏感資訊的隱藏,以及適時使用 __post_init__ 進行驗證,就能避免常見陷阱,寫出 健全且易於擴充 的程式碼。

希望本篇文章讓你對資料類別有了完整的概念,並能在自己的 Python 專案中 活用 @dataclass,提升開發效率與程式品質。祝 coding 順利!