5.1 異常處理策略
5.1 異常處理策略
異常處理是撰寫穩健程式碼的關鍵。本章介紹 Python 的異常處理機制,以及 Hook 系統中採用的設計策略。
基本語法
try-except
1try:
2 result = risky_operation()
3except SomeException as e:
4 handle_error(e)完整結構
1try:
2 result = risky_operation()
3except SpecificError as e:
4 # 處理特定錯誤
5 handle_specific_error(e)
6except (TypeError, ValueError) as e:
7 # 處理多種錯誤
8 handle_type_error(e)
9except Exception as e:
10 # 處理其他所有錯誤
11 handle_unknown_error(e)
12else:
13 # 沒有錯誤時執行
14 process_result(result)
15finally:
16 # 無論如何都執行
17 cleanup()常見異常類型
| 異常 | 發生時機 |
|---|---|
FileNotFoundError | 檔案不存在 |
PermissionError | 權限不足 |
ValueError | 值不合法 |
TypeError | 型別不正確 |
KeyError | 字典鍵不存在 |
IndexError | 索引超出範圍 |
JSONDecodeError | JSON 解析失敗 |
TimeoutError | 操作超時 |
實際範例:Git 命令執行
來自 .claude/lib/git_utils.py:
1import subprocess
2from typing import Optional
3
4def run_git_command(
5 args: list[str],
6 cwd: Optional[str] = None,
7 timeout: int = 10
8) -> tuple[bool, str]:
9 """
10 執行 git 命令並返回結果
11
12 Returns:
13 tuple[bool, str]: (是否成功, 輸出內容或錯誤訊息)
14 """
15 try:
16 result = subprocess.run(
17 ["git"] + args,
18 cwd=cwd,
19 capture_output=True,
20 text=True,
21 timeout=timeout
22 )
23 if result.returncode == 0:
24 return True, result.stdout.strip()
25 else:
26 return False, result.stderr.strip()
27
28 except subprocess.TimeoutExpired:
29 # 命令超時
30 return False, f"Command timed out after {timeout}s"
31
32 except FileNotFoundError:
33 # git 命令不存在
34 return False, "git command not found"
35
36 except Exception as e:
37 # 其他未預期的錯誤
38 return False, str(e)設計分析
這個函式展示了 Hook 系統的異常處理策略:
- 不拋出異常:返回
(bool, str)元組 - 捕獲特定異常:分別處理
TimeoutExpired和FileNotFoundError - 兜底處理:
except Exception捕獲其他錯誤 - 提供有意義的錯誤訊息:讓呼叫者知道發生了什麼
Hook 系統的異常哲學
為什麼不直接拋出異常?
Hook 腳本需要穩定運行,即使遇到錯誤也要:
- 給出有意義的反饋
- 不中斷整個 Claude 工作流程
- 讓主程式能夠決定如何處理
(bool, str) 返回值模式
1# 函式簽名
2def validate_something() -> tuple[bool, str]:
3 """
4 Returns:
5 tuple[bool, str]: (成功與否, 訊息)
6 """
7 pass
8
9# 使用方式
10success, message = validate_something()
11if success:
12 print(f"成功: {message}")
13else:
14 print(f"失敗: {message}")實際應用
1def read_hook_input() -> dict:
2 """
3 從 stdin 讀取 Hook 輸入
4
5 Returns:
6 dict: 解析後的 JSON 資料,解析失敗時返回空字典
7 """
8 try:
9 return json.load(sys.stdin)
10 except json.JSONDecodeError:
11 return {} # 不拋出異常,返回安全的預設值
12 except Exception:
13 return {}異常處理策略
策略 1:捕獲並轉換
將異常轉換為返回值:
1def safe_divide(a: float, b: float) -> tuple[bool, float]:
2 try:
3 return True, a / b
4 except ZeroDivisionError:
5 return False, 0.0策略 2:捕獲並記錄
記錄後繼續執行:
1def process_files(files: list[str]) -> list[str]:
2 results = []
3 for file in files:
4 try:
5 result = process_file(file)
6 results.append(result)
7 except Exception as e:
8 logger.error(f"Failed to process {file}: {e}")
9 # 繼續處理其他檔案
10 return results策略 3:捕獲並重新拋出
添加上下文後重新拋出:
1def load_config(path: str) -> dict:
2 try:
3 with open(path) as f:
4 return json.load(f)
5 except FileNotFoundError:
6 raise FileNotFoundError(f"Config file not found: {path}")
7 except json.JSONDecodeError as e:
8 raise ValueError(f"Invalid JSON in {path}: {e}")策略 4:使用預設值
提供安全的預設值:
1def get_config_value(config: dict, key: str, default: str = "") -> str:
2 try:
3 return config[key]
4 except KeyError:
5 return default最佳實踐
1. 具體優於籠統
1# 好:捕獲具體異常
2try:
3 data = json.load(f)
4except json.JSONDecodeError:
5 data = {}
6
7# 不好:捕獲所有異常
8try:
9 data = json.load(f)
10except Exception: # 可能隱藏其他問題
11 data = {}2. 保留原始異常資訊
1# 好:保留原始異常
2try:
3 process()
4except ValueError as e:
5 raise RuntimeError(f"Processing failed: {e}") from e
6
7# 不好:丟失原始資訊
8try:
9 process()
10except ValueError:
11 raise RuntimeError("Processing failed")3. 使用 finally 清理資源
1f = None
2try:
3 f = open("file.txt")
4 process(f)
5except IOError:
6 handle_error()
7finally:
8 if f:
9 f.close() # 確保關閉檔案
10
11# 更好:使用 with 語句
12with open("file.txt") as f:
13 process(f) # 自動關閉4. 不要靜默忽略異常
1# 不好:完全忽略錯誤
2try:
3 risky_operation()
4except Exception:
5 pass # 什麼都不做
6
7# 好:至少記錄一下
8try:
9 risky_operation()
10except Exception as e:
11 logger.warning(f"Operation failed (ignored): {e}")自訂異常
1class HookError(Exception):
2 """Hook 基礎異常"""
3 pass
4
5class ValidationError(HookError):
6 """驗證錯誤"""
7 pass
8
9class ConfigurationError(HookError):
10 """配置錯誤"""
11 pass
12
13# 使用
14def validate_hook(path: str) -> None:
15 if not path.endswith(".py"):
16 raise ValidationError(f"Invalid hook file: {path}")思考題
- 為什麼
run_git_command不直接拋出異常? - 什麼情況下應該使用
except Exception? from e在raise ... from e中的作用是什麼?
實作練習
- 重構一個使用異常的函式,改為返回
(bool, str)模式 - 實作一個函式,嘗試多種方式載入配置(JSON、YAML、預設值)
- 寫一個裝飾器,自動捕獲異常並轉換為返回值
下一章:返回值設計