2.3 Dataclass 資料結構
2.3 Dataclass 資料結構
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 的比較
| 特性 | dataclass | TypedDict |
|---|---|---|
| 用途 | 資料物件 | 字典型別提示 |
| 執行時驗證 | 有(可選) | 無 |
| 方法 | 可以定義 | 不能定義 |
| 輸出 | 物件 | 字典 |
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] = None2. 使用 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) # 自動計算思考題
- 為什麼
issues: List[str] = []是危險的? __post_init__和在__init__中計算有什麼區別?- 什麼時候應該使用
frozen=True?
實作練習
- 建立一個
HookResultdataclass,包含 hook 名稱、執行時間、成功狀態 - 實作一個 dataclass,使用
__post_init__計算衍生欄位 - 將現有的字典結構重構為 dataclass
上一章:Optional、Union、泛型 下一章:Enum 列舉型別