一. 前言 #
拍拍君剛開始寫 Python 的時候,每次定義一個類別來存資料,都要手動寫 __init__、__repr__、__eq__⋯⋯一個簡單的類別搞下來就二三十行,還容易打錯字 😫
後來發現 Python 3.7 開始內建了一個超棒的模組——dataclasses,只要加一個裝飾器,Python 就幫你自動生成那些重複的程式碼!
今天就來帶大家從零開始學 dataclasses,寫出更乾淨、更 Pythonic 的程式碼 ✨
二. 沒有 dataclass 的痛苦 #
先來看看傳統寫法有多累:
class Student:
def __init__(self, name: str, age: int, grade: float, school: str = "拍拍大學"):
self.name = name
self.age = age
self.grade = grade
self.school = school
def __repr__(self):
return f"Student(name={self.name!r}, age={self.age}, grade={self.grade}, school={self.school!r})"
def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return (self.name, self.age, self.grade, self.school) == (
other.name, other.age, other.grade, other.school
)
光是一個存學生資料的類別就要 17 行,而且每個欄位的名字重複出現了好幾次。如果之後要加一個欄位,至少要改三個地方。這不是人幹的事 🤯
三. 用 dataclass 改寫 #
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
grade: float
school: str = "拍拍大學"
就這樣。8 行搞定,而且自動幫你生成 __init__、__repr__、__eq__!
來試試看:
s1 = Student("拍拍君", 20, 95.5)
s2 = Student("拍拍君", 20, 95.5)
print(s1)
# Student(name='拍拍君', age=20, grade=95.5, school='拍拍大學')
print(s1 == s2)
# True(自動比較所有欄位)
是不是超讚?少寫了一半以上的程式碼,功能完全一樣!
四. dataclass 的參數選項 #
@dataclass 裝飾器有幾個常用參數可以調整行為:
@dataclass(
init=True, # 自動生成 __init__(預設 True)
repr=True, # 自動生成 __repr__(預設 True)
eq=True, # 自動生成 __eq__(預設 True)
order=False, # 自動生成 __lt__, __le__, __gt__, __ge__
frozen=False, # 讓實例變成不可變(唯讀)
slots=False, # 使用 __slots__(Python 3.10+)
)
class MyData:
...
frozen:不可變的資料類別 #
如果你的資料建立後不該被修改,用 frozen=True:
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
p.x = 3.0 # ❌ FrozenInstanceError!
這在多執行緒或函式傳遞的情境下特別有用,保證資料不會被意外修改。
order:支援排序比較 #
@dataclass(order=True)
class Score:
value: float
name: str
scores = [Score(88, "小明"), Score(95, "拍拍君"), Score(72, "小華")]
print(sorted(scores))
# [Score(value=72, name='小華'), Score(value=88, name='小明'), Score(value=95, name='拍拍君')]
slots:更省記憶體(Python 3.10+) #
@dataclass(slots=True)
class Config:
host: str
port: int
debug: bool = False
加上 slots=True 後,Python 會用 __slots__ 取代 __dict__,存取速度更快、記憶體用量更少。如果你有大量實例,這個很有感!
五. field() 進階欄位設定 #
有時候你需要更精細地控制每個欄位,這時候就用 field():
from dataclasses import dataclass, field
@dataclass
class Team:
name: str
members: list[str] = field(default_factory=list)
_internal_id: int = field(default=0, repr=False)
為什麼不能寫 members: list = []?
#
# ❌ 千萬不要這樣寫!
@dataclass
class BadTeam:
members: list = [] # ValueError: mutable default
Python 會直接報錯!因為 mutable(可變)的預設值會被所有實例共享,造成恐怖的 bug。所以要改用 field(default_factory=list)。
field() 常用參數 #
field(
default=..., # 預設值(跟 mutable default 互斥)
default_factory=..., # 用函式生成預設值(每次建立新實例都會呼叫)
repr=True, # 是否出現在 __repr__ 裡
compare=True, # 是否參與 __eq__ 比較
hash=None, # 是否參與 __hash__
init=True, # 是否出現在 __init__ 參數裡
metadata=None, # 附加的 metadata dict
)
實用範例:自動生成 ID #
from dataclasses import dataclass, field
from uuid import uuid4
@dataclass
class Task:
title: str
description: str = ""
task_id: str = field(default_factory=lambda: uuid4().hex[:8])
t1 = Task("寫部落格")
t2 = Task("喝咖啡")
print(t1.task_id) # 例如 'a3f2c1b8'
print(t2.task_id) # 例如 '7e9d4f1a'(每次都不同)
六. 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
r = Rectangle(3.0, 4.0)
print(r)
# Rectangle(width=3.0, height=4.0, area=12.0)
搭配驗證使用 #
@dataclass
class Age:
value: int
def __post_init__(self):
if self.value < 0 or self.value > 150:
raise ValueError(f"不合理的年齡:{self.value}")
Age(25) # ✅ OK
Age(-5) # ❌ ValueError: 不合理的年齡:-5
七. 繼承 #
dataclass 支援繼承,子類別會繼承父類別的所有欄位:
@dataclass
class Animal:
name: str
sound: str
@dataclass
class Pet(Animal):
owner: str
vaccinated: bool = True
pet = Pet("小拍", "喵", "拍拍君")
print(pet)
# Pet(name='小拍', sound='喵', owner='拍拍君', vaccinated=True)
⚠️ 注意:如果父類別有帶預設值的欄位,子類別的新欄位也必須有預設值,不然 Python 會報錯(因為有預設值的參數不能出現在沒有預設值的參數前面)。
八. 實用函式 #
dataclasses 模組提供了幾個好用的工具函式:
asdict / astuple:轉換成 dict 或 tuple #
from dataclasses import asdict, astuple
@dataclass
class Config:
host: str
port: int
debug: bool = False
config = Config("localhost", 8080, True)
print(asdict(config))
# {'host': 'localhost', 'port': 8080, 'debug': True}
print(astuple(config))
# ('localhost', 8080, True)
這在要把資料序列化成 JSON 的時候超方便:
import json
json_str = json.dumps(asdict(config), ensure_ascii=False)
print(json_str)
# {"host": "localhost", "port": 8080, "debug": true}
fields:取得所有欄位資訊 #
from dataclasses import fields
for f in fields(config):
print(f" {f.name}: {f.type} = {f.default}")
host: <class 'str'> = <dataclasses.MISSING>
port: <class 'int'> = <dataclasses.MISSING>
debug: <class 'bool'> = False
replace:建立修改過的副本 #
from dataclasses import replace
config2 = replace(config, port=9090, debug=False)
print(config2)
# Config(host='localhost', port=9090, debug=False)
print(config) # 原本的不受影響
# Config(host='localhost', port=8080, debug=True)
replace 特別適合 frozen=True 的 dataclass,因為你不能直接改屬性,只能建立新的。
九. dataclass vs NamedTuple vs Pydantic #
拍拍君幫你整理一下什麼時候用哪個:
dataclass
- ✅ 標準庫,不需額外安裝
- ✅ 支援 mutable 和 frozen
- ✅ 支援繼承、方法、
__post_init__ - ❌ 沒有自動型別驗證
NamedTuple
- ✅ 不可變,天生 hashable
- ✅ 可以解包(unpacking)
- ❌ 不支援預設的 mutable 欄位
Pydantic
- ✅ 自動型別驗證和轉換
- ✅ 強大的 JSON schema 支援
- ❌ 需要額外安裝
拍拍君的建議:
- 簡單的資料容器 → dataclass
- 需要型別驗證的 API 資料 → Pydantic
- 需要不可變 + hashable →
frozen=True的 dataclass 或 NamedTuple
十. 實戰:用 dataclass 管理設定檔 #
最後來一個完整的實戰範例,用 dataclass 打造一個簡單的設定檔管理系統:
from dataclasses import dataclass, field, asdict
from pathlib import Path
import json
@dataclass
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
name: str = "pypy_db"
user: str = "admin"
password: str = field(default="changeme", repr=False) # 密碼不顯示在 repr 裡
@dataclass
class AppConfig:
app_name: str = "拍拍君的應用程式"
debug: bool = False
version: str = "1.0.0"
db: DatabaseConfig = field(default_factory=DatabaseConfig)
allowed_hosts: list[str] = field(default_factory=lambda: ["localhost"])
def save(self, path: str | Path) -> None:
"""儲存設定到 JSON 檔案"""
Path(path).write_text(
json.dumps(asdict(self), ensure_ascii=False, indent=2),
encoding="utf-8",
)
@classmethod
def load(cls, path: str | Path) -> "AppConfig":
"""從 JSON 檔案載入設定"""
data = json.loads(Path(path).read_text(encoding="utf-8"))
db_data = data.pop("db", {})
return cls(db=DatabaseConfig(**db_data), **data)
# 使用
config = AppConfig(debug=True, allowed_hosts=["localhost", "0.0.0.0"])
print(config)
# AppConfig(app_name='拍拍君的應用程式', debug=True, version='1.0.0',
# db=DatabaseConfig(host='localhost', port=5432, name='pypy_db', user='admin'),
# allowed_hosts=['localhost', '0.0.0.0'])
config.save("config.json")
loaded = AppConfig.load("config.json")
print(config == loaded) # True
注意密碼欄位用了 repr=False,這樣 print 的時候不會不小心洩漏密碼,是個很實用的小技巧 🔒
結語 #
dataclasses 是 Python 標準庫裡的隱藏寶石 💎。它不需要安裝任何第三方套件,就能讓你的類別定義變得簡潔又清晰。
拍拍君覺得它最棒的地方在於——你可以把注意力放在「這個類別有哪些資料」,而不是「要怎麼寫那些 boilerplate 程式碼」。
如果你的類別主要是用來存資料的,沒有複雜的型別驗證需求,dataclass 絕對是你的首選。快去試試吧!🐍