本文 AI 產出,尚未審核

Python:型別提示與靜態分析 – dataclass + Type Hint


簡介

在 Python 3.6 之後,型別提示(type hint) 逐漸成為程式碼可讀性與維護性的核心工具。配合靜態分析工具(如 mypypyright)使用,能在執行前即捕捉到許多潛在錯誤,讓開發者在編寫程式時就能得到即時的型別檢查回饋。

另一方面,dataclass(自 Python 3.7 正式納入標準庫)提供了一種簡潔的方式來宣告「只用來儲存資料」的類別。它會自動產生 __init____repr____eq__ 等魔法方法,減少樣板程式碼。

dataclass型別提示 結合,能同時獲得:

  • 清晰的資料結構說明(型別提示)
  • 自動產生的建構子與比較功能dataclass
  • 靜態分析工具的完整支援(如未符合型別會直接報錯)

對於專案規模從小型腳本到大型服務端程式,都能提升開發效率與程式品質。本篇文章將從核心概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用場景,幫助你在日常開發中善用 dataclass + type hint。


核心概念

1. dataclass 的基本語法

from dataclasses import dataclass

@dataclass
class Point:
    x: float          # 透過型別提示宣告欄位型別
    y: float
  • @dataclass 會根據欄位自動產生 __init__(self, x: float, y: float)__repr____eq__ 等。
  • 欄位的型別提示(x: float)不會在執行時強制轉型,僅供型別檢查工具與 IDE 使用。

重點:即使不寫 __init__dataclass 仍會根據提示自動產生,讓程式碼更簡潔。


2. 預設值與 field()

from dataclasses import dataclass, field
from typing import List

@dataclass
class Todo:
    title: str
    completed: bool = False                     # 預設值
    tags: List[str] = field(default_factory=list)  # 使用 factory 產生可變預設值
  • 不可變的預設值(如 False0"")直接寫在欄位後即可。
  • 可變的預設值(如 list、dict)必須使用 field(default_factory=…),避免所有實例共享同一個物件。

3. 型別提示的進階寫法

3.1 使用 typing 模組的泛型

from dataclasses import dataclass
from typing import Generic, TypeVar, List

T = TypeVar('T')

@dataclass
class Box(Generic[T]):
    content: T
    history: List[T] = field(default_factory=list)
  • Box[int]Box[str] 等都能在靜態分析時得到正確的型別推斷。
  • Genericdataclass 也能支援 型別參數化,適合實作容器、結果封裝等結構。

3.2 OptionalUnion

from dataclasses import dataclass
from typing import Optional, Union

@dataclass
class User:
    id: int
    name: str
    email: Optional[str] = None                 # 允許 None
    role: Union[str, int] = "guest"             # 可接受字串或整數
  • Optional[X] 等價於 Union[X, None],提醒開發者此欄位可能是 None
  • Union 讓欄位接受多種型別,靜態分析會檢查是否符合其中任一型別。

4. frozen=True:讓資料類別成為不可變物件

@dataclass(frozen=True)
class ImmutablePoint:
    x: float
    y: float
  • frozen=True 會在 __setattr__ 加入保護,防止在建立後修改屬性。
  • 這在 資料傳遞(data transfer objects, DTO)快取鍵 時非常有用,因為不可變物件天然可作為字典 key。

5. 結合靜態分析工具

# 安裝 mypy
pip install mypy

# 執行型別檢查
mypy my_project/

在上述範例中,如果 Todo.tags 被指定為非 listmypy 會直接報錯:

error: Incompatible type for "tags" (expected "List[str]", got "int")

這樣的即時回饋可大幅降低執行時錯誤的機會。


程式碼範例

以下提供 5 個實務導向的範例,說明 dataclass + type hint 在不同情境下的寫法與技巧。每段程式碼都附有說明註解。

範例 1:簡易的 API 回傳模型

from dataclasses import dataclass
from typing import List, Optional

@dataclass
class ArticleResponse:
    id: int
    title: str
    author: str
    tags: List[str]
    summary: Optional[str] = None
  • 這個模型可直接作為 FastAPI、Flask 等框架的回傳型別。
  • Optional[str] 告訴前端「summary 可能不存在」,IDE 會自動提示 None 判斷。

範例 2:使用 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:
        """將指定 key 的計數加上 amount。"""
        self.counts[key] = self.counts.get(key, 0) + amount
  • 每個 Counter 實例都有自己的 counts dict,避免共用同一個空字典。
  • inc 方法的參數也使用型別提示,靜態分析會檢查 key 必須是 str

範例 3:不可變的設定物件(適合作為全域設定)

from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    host: str
    port: int
    debug: bool = False
  • Config 建立後不可變,確保程式在執行期間不會意外改變設定值。
  • 可直接作為函式參數傳遞,且可安全作為字典的 key:
cfg = Config(host="localhost", port=8000)
cache = {cfg: "已初始化"}

範例 4:泛型容器 – Result 包裝成功或失敗資訊

from dataclasses import dataclass
from typing import Generic, TypeVar, Optional

T = TypeVar('T')
E = TypeVar('E')

@dataclass
class Result(Generic[T, E]):
    value: Optional[T] = None
    error: Optional[E] = None

    @property
    def is_ok(self) -> bool:
        return self.error is None

    @property
    def is_err(self) -> bool:
        return self.error is not None
  • 使用方式:
def divide(a: float, b: float) -> Result[float, str]:
    if b == 0:
        return Result(error="除以零")
    return Result(value=a / b)

res = divide(10, 2)
if res.is_ok:
    print("結果 =", res.value)
else:
    print("錯誤 :", res.error)
  • 靜態分析會確保 valueerror 的型別分別符合 floatstr

範例 5:結合 FastAPI 的請求與回傳模型

from fastapi import FastAPI
from pydantic import BaseModel
from dataclasses import dataclass
from typing import List

app = FastAPI()

# Pydantic 用於驗證請求資料
class CreateUserReq(BaseModel):
    username: str
    age: int

# 回傳使用 dataclass
@dataclass
class UserDTO:
    id: int
    username: str
    age: int
    tags: List[str]

# 模擬資料庫
_FAKE_DB: dict[int, UserDTO] = {}
_NEXT_ID = 1

@app.post("/users/", response_model=UserDTO)
def create_user(req: CreateUserReq):
    global _NEXT_ID
    user = UserDTO(id=_NEXT_ID, username=req.username, age=req.age, tags=[])
    _FAKE_DB[_NEXT_ID] = user
    _NEXT_ID += 1
    return user
  • 重點:雖然 FastAPI 主要使用 Pydantic,但回傳時可以直接使用 dataclass,FastAPI 會自動把它轉換成 JSON。
  • 這樣的寫法讓資料層(UserDTO)保持簡潔,同時仍享有型別提示與自動生成的 __repr__

常見陷阱與最佳實踐

陷阱 說明 解決方式
可變預設值共享 使用 tags: List[str] = [] 會讓所有實例共享同一個列表。 改用 field(default_factory=list)
型別提示僅供靜態分析 直接把錯誤型別傳入建構子不會拋出例外,只會在 mypy 報錯。 在開發流程中加入 mypy CI,確保每次提交都有型別檢查。
frozen=True 與可變欄位衝突 即使 frozen=True,若欄位本身是可變物件(如 list),仍可在外部修改。 使用 typing.Tuple 取代 list,或自行在 __post_init__ 內作深拷貝。
__post_init__ 中的型別檢查 有時需要在建構後做更嚴格的檢查,但忘記加上 @dataclassinit=False 會導致參數不匹配。 若需要自訂初始化邏輯,使用 def __post_init__(self): 並在其中執行驗證。
混用 dataclasspydantic 兩者都會自動產生 __init__,若同時繼承會產生衝突。 只在資料層使用 dataclass,在 API 層使用 Pydantic,或使用 pydantic.dataclasses.dataclass 取代標準 dataclass

最佳實踐

  1. 盡量在類別層面完整描述型別:所有欄位都加上提示,讓 IDE 能提供自動完成與錯誤提示。
  2. 使用 field() 管理預設值:尤其是可變物件,避免不易察覺的共享問題。
  3. 結合 CI/CD 執行型別檢查:例如在 GitHub Actions 中加入 mypy --strict,確保每次合併前通過。
  4. 在不可變資料結構上使用 frozen=True:提升程式的可預測性與雜湊相容性(可作為 dict key)。
  5. dataclass 作為「純資料」的 DTO:業務邏輯應寫在服務層或領域模型中,保持 dataclass 的簡潔與專注。

實際應用場景

場景 為何適合使用 dataclass + type hint
API 回傳/請求模型 直接對外暴露的資料結構,型別提示讓前端與測試人員清楚欄位型別。
設定檔與環境變數 frozen=True 的設定類別保證在程式執行期間不會被意外改變。
快取鍵(Cache Key) 不可變的 dataclass 可直接作為 dictlru_cache 的鍵,避免自行實作 __hash__
工作流(Workflow)或排程任務資料 使用 default_factory 管理可變欄位,確保每個任務都有獨立的狀態。
領域驅動設計(DDD)中的值物件(Value Object) 值物件本質上是不可變且只關心其屬性,正好符合 @dataclass(frozen=True) 的特性。

總結

  • dataclass 為 Python 提供了「少寫樣板,多寫業務」的便利;配合 型別提示,能在編寫階段即捕捉錯誤、提升可讀性。
  • 正確使用 field(default_factory=…)frozen=True、以及 typing 系列(ListOptionalGeneric 等)可以讓資料類別在各種情境下都保持安全且易於維護。
  • 結合靜態分析工具(mypypyright)與 CI 流程,讓型別檢查成為開發的必備步驟,從根本減少執行時錯誤。
  • 無論是 API、設定、快取或是領域模型,dataclass + type hint 都是「清晰、可靠且高效」的解決方案。

把今天學到的概念套用到自己的專案中,從 小範例 開始逐步改寫,讓程式碼在可讀性與安全性上都有顯著提升吧!祝你寫程式愉快 🚀