快轉到主要內容
  1. 教學文章/

少寫一半程式碼:dataclasses 讓你的 Python 類別煥然一新

·6 分鐘· loading · loading · ·
Python Dataclasses Oop 標準庫
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 17: 本文

一. 前言
#

拍拍君剛開始寫 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=TruedataclassNamedTuple

十. 實戰:用 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 絕對是你的首選。快去試試吧!🐍

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 17: 本文

相關文章

Ruff:用 Rust 寫的 Python Linter,快到你會懷疑人生
·4 分鐘· loading · loading
Python Ruff Linter Formatter Code-Quality
pathlib:優雅處理檔案路徑的現代方式
·6 分鐘· loading · loading
Python Pathlib 檔案處理 標準庫
超快速 Python 套件管理:uv 完全教學
·6 分鐘· loading · loading
Python Uv Package Manager Rust
Python 資料驗證小幫手:Pydantic
·4 分鐘· loading · loading
Python Pydantic Data Validation
科學計算:數值積分
·5 分鐘· loading · loading
Python Numpy Scipy Numerical Methods Numerical Integral
讓你的終端機華麗變身:Rich 套件教學
·2 分鐘· loading · loading
Python Rich Cli