dataclass 是 Python 3.7+ 引入的裝飾器,用於快速建立主要用於存放資料的類別。它自動產生 __init____repr__ 等方法,減少樣板程式碼。

為什麼使用 Dataclass?

傳統類別

 1class ValidationIssue:
 2    def __init__(self, level, message, line=None, suggestion=None):
 3        self.level = level
 4        self.message = message
 5        self.line = line
 6        self.suggestion = suggestion
 7
 8    def __repr__(self):
 9        return f"ValidationIssue(level={self.level!r}, message={self.message!r})"
10
11    def __eq__(self, other):
12        if not isinstance(other, ValidationIssue):
13            return False
14        return (self.level == other.level and
15                self.message == other.message and
16                self.line == other.line and
17                self.suggestion == other.suggestion)

使用 Dataclass

1from dataclasses import dataclass
2from typing import Optional
3
4@dataclass
5class ValidationIssue:
6    level: str
7    message: str
8    line: Optional[int] = None
9    suggestion: Optional[str] = None

自動產生:

  • __init__
  • __repr__
  • __eq__

實際範例:Hook 驗證器

來自 .claude/lib/hook_validator.py

 1from dataclasses import dataclass, field
 2from typing import Optional, List
 3
 4
 5@dataclass
 6class ValidationIssue:
 7    """驗證問題描述"""
 8    level: str          # "error" | "warning" | "info"
 9    message: str
10    line: Optional[int] = None
11    suggestion: Optional[str] = None
12
13
14@dataclass
15class ValidationResult:
16    """單個 Hook 的驗證結果"""
17    hook_path: str
18    issues: List[ValidationIssue] = field(default_factory=list)
19    is_compliant: bool = True
20
21    def __post_init__(self):
22        """計算 is_compliant 狀態"""
23        self.is_compliant = not any(
24            issue.level == "error" for issue in self.issues
25        )

基本語法

欄位定義

 1from dataclasses import dataclass
 2from typing import List, Optional
 3
 4@dataclass
 5class Person:
 6    # 必要欄位(無預設值)
 7    name: str
 8    age: int
 9
10    # 可選欄位(有預設值)
11    email: Optional[str] = None
12    tags: List[str] = None  # 錯誤!見下方說明

可變預設值的問題

 1from dataclasses import dataclass, field
 2from typing import List
 3
 4# 錯誤:可變物件作為預設值
 5@dataclass
 6class Wrong:
 7    items: List[str] = []  # 所有實例會共用同一個列表!
 8
 9# 正確:使用 field(default_factory=...)
10@dataclass
11class Correct:
12    items: List[str] = field(default_factory=list)

field() 函式

field() 提供更多欄位配置選項:

 1from dataclasses import dataclass, field
 2from typing import List
 3
 4@dataclass
 5class ValidationResult:
 6    hook_path: str
 7    # 使用 default_factory 建立可變預設值
 8    issues: List[ValidationIssue] = field(default_factory=list)
 9    # 不包含在 __repr__ 中
10    _internal: str = field(default="", repr=False)
11    # 不包含在比較中
12    cached: bool = field(default=False, compare=False)

field() 參數

參數說明預設值
default預設值
default_factory產生預設值的函式
repr是否包含在 __repr__True
compare是否包含在比較中True
hash是否包含在 hash 中None
init是否包含在 __init__True

post_init

__init__ 完成後執行,用於衍生欄位計算:

 1from dataclasses import dataclass, field
 2from typing import List
 3
 4@dataclass
 5class ValidationResult:
 6    hook_path: str
 7    issues: List[ValidationIssue] = field(default_factory=list)
 8    is_compliant: bool = True
 9
10    def __post_init__(self):
11        """根據 issues 計算 is_compliant"""
12        self.is_compliant = not any(
13            issue.level == "error" for issue in self.issues
14        )
15
16# 使用
17result = ValidationResult(
18    hook_path="my_hook.py",
19    issues=[ValidationIssue(level="error", message="Bad")]
20)
21print(result.is_compliant)  # False(自動計算)

不可變 Dataclass

使用 frozen=True 建立不可變物件:

1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class Point:
5    x: float
6    y: float
7
8p = Point(1.0, 2.0)
9p.x = 3.0  # 錯誤!FrozenInstanceError

不可變 dataclass 可以用作字典鍵或集合元素。

轉換為字典

使用 asdict() 轉換為字典:

 1from dataclasses import dataclass, asdict
 2
 3@dataclass
 4class Config:
 5    name: str
 6    timeout: int
 7
 8config = Config("test", 30)
 9config_dict = asdict(config)
10# {'name': 'test', 'timeout': 30}
11
12# 用於 JSON 序列化
13import json
14json.dumps(asdict(config))

實際應用:Markdown 連結檢查

來自 .claude/lib/markdown_link_checker.py

 1from dataclasses import dataclass, asdict, field
 2from typing import List
 3
 4@dataclass
 5class BrokenLink:
 6    """失效連結描述"""
 7    file: str
 8    line: int
 9    link_text: str
10    link_target: str
11    suggestion: str = ""
12
13
14@dataclass
15class LinkCheckResult:
16    """單個檔案的連結檢查結果"""
17    file_path: str
18    total_links: int
19    broken_links: List[BrokenLink] = field(default_factory=list)
20    is_valid: bool = True
21
22    def __post_init__(self):
23        """計算 is_valid 狀態"""
24        self.is_valid = len(self.broken_links) == 0
25
26
27# 使用範例
28result = LinkCheckResult(
29    file_path="docs/README.md",
30    total_links=10,
31    broken_links=[
32        BrokenLink(
33            file="docs/README.md",
34            line=15,
35            link_text="Guide",
36            link_target="./guide.md",
37            suggestion="檔案不存在"
38        )
39    ]
40)
41
42# 輸出 JSON
43print(json.dumps(asdict(result), ensure_ascii=False, indent=2))

與 TypedDict 的比較

特性dataclassTypedDict
用途資料物件字典型別提示
執行時驗證有(可選)
方法可以定義不能定義
輸出物件字典
 1from typing import TypedDict
 2from dataclasses import dataclass
 3
 4# TypedDict:給字典加型別
 5class ConfigDict(TypedDict):
 6    name: str
 7    timeout: int
 8
 9config: ConfigDict = {"name": "test", "timeout": 30}
10
11# dataclass:建立資料物件
12@dataclass
13class Config:
14    name: str
15    timeout: int
16
17config = Config(name="test", timeout=30)

最佳實踐

1. 必要欄位放前面

1@dataclass
2class Person:
3    # 必要欄位
4    name: str
5    age: int
6    # 可選欄位
7    email: Optional[str] = None

2. 使用 field() 處理可變預設值

1@dataclass
2class Container:
3    items: List[str] = field(default_factory=list)  # 正確
4    # items: List[str] = []  # 錯誤!

3. 善用 post_init

1@dataclass
2class Result:
3    items: List[str] = field(default_factory=list)
4    count: int = 0
5
6    def __post_init__(self):
7        self.count = len(self.items)  # 自動計算

思考題

  1. 為什麼 issues: List[str] = [] 是危險的?
  2. __post_init__ 和在 __init__ 中計算有什麼區別?
  3. 什麼時候應該使用 frozen=True

實作練習

  1. 建立一個 HookResult dataclass,包含 hook 名稱、執行時間、成功狀態
  2. 實作一個 dataclass,使用 __post_init__ 計算衍生欄位
  3. 將現有的字典結構重構為 dataclass

上一章:Optional、Union、泛型 下一章:Enum 列舉型別