異常處理是撰寫穩健程式碼的關鍵。本章介紹 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索引超出範圍
JSONDecodeErrorJSON 解析失敗
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 系統的異常處理策略:

  1. 不拋出異常:返回 (bool, str) 元組
  2. 捕獲特定異常:分別處理 TimeoutExpiredFileNotFoundError
  3. 兜底處理except Exception 捕獲其他錯誤
  4. 提供有意義的錯誤訊息:讓呼叫者知道發生了什麼

Hook 系統的異常哲學

為什麼不直接拋出異常?

Hook 腳本需要穩定運行,即使遇到錯誤也要:

  1. 給出有意義的反饋
  2. 不中斷整個 Claude 工作流程
  3. 讓主程式能夠決定如何處理

(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}")

思考題

  1. 為什麼 run_git_command 不直接拋出異常?
  2. 什麼情況下應該使用 except Exception
  3. from eraise ... from e 中的作用是什麼?

實作練習

  1. 重構一個使用異常的函式,改為返回 (bool, str) 模式
  2. 實作一個函式,嘗試多種方式載入配置(JSON、YAML、預設值)
  3. 寫一個裝飾器,自動捕獲異常並轉換為返回值

下一章:返回值設計