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] = {},c1與c2會共享同一個字典,導致資料互相污染。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) 只繼承部分功能) |
最佳實踐
- 盡量使用型別註記:
dataclass的核心在於型別資訊,能讓 IDE、型別檢查工具(mypy)發揮功效。 - 預設值使用
default_factory:特別是可變容器(list、dict、set)或自訂類別。 - 將不可變的資料類別設為
frozen=True:有助於安全的快取鍵、集合元素。 - 在需要自訂驗證時使用
__post_init__,避免在__init__中寫過長的程式碼。 - 保持欄位宣告順序與業務重要度一致:排序比較會依此順序進行,讓程式行為更直觀。
實際應用場景
| 場景 | 為何適合使用 @dataclass |
|---|---|
| API 輸入/輸出模型 | 可以快速定義 JSON 反序列化/序列化的資料結構,配合 pydantic 或 marshmallow 使用。 |
| 遊戲開發的實體(Entity) | 如座標、速度、生命值等屬性,使用 frozen=False 讓狀態可變,且自動產生 __repr__ 方便除錯。 |
| 金融交易的不可變記錄 | 交易編號、時間、金額等欄位設為 frozen=True,保證交易資料在程式運行期間不會被意外修改。 |
| 排程系統的任務描述 | order=True 可直接以優先權排序任務列表,省去自行實作比較邏輯。 |
| 設定檔或環境變數的封裝 | 把環境變數映射成資料類別,使用 frozen=True 防止程式中途改變設定值。 |
總結
@dataclass 為 Python 提供了一套 簡潔、功能完整 的資料類別實作方式。透過自動生成的 __init__、__repr__、__eq__,以及可選的 order、frozen、slots 等參數,我們可以在 數行程式碼 內建立安全、可讀、易維護的模型。
在實務開發中,從 API 請求模型、遊戲實體、金融交易記錄,到排程任務與全域設定,dataclass 都能大幅減少樣板程式,讓開發者把精力集中在 業務邏輯 上。只要留意可變預設值、敏感資訊的隱藏,以及適時使用 __post_init__ 進行驗證,就能避免常見陷阱,寫出 健全且易於擴充 的程式碼。
希望本篇文章讓你對資料類別有了完整的概念,並能在自己的 Python 專案中 活用 @dataclass,提升開發效率與程式品質。祝 coding 順利!