Python 的模組系統是組織程式碼的基礎。理解模組如何運作,是維護和擴展 Hook 系統的關鍵。

承接提示:如果你還不熟悉程式如何從單一 .py 檔案拆成多個 module,請先閱讀 從單一 script 到多檔案專案。本章聚焦在已經出現 package 後,如何理解 module、__init__.py 與公開 API。

基本概念

模組(Module)

一個 .py 檔案就是一個模組。例如 git_utils.py 就是 git_utils 模組。

1# git_utils.py 是一個模組
2# 可以被其他檔案導入
3
4from git_utils import get_current_branch

套件(Package)

包含 __init__.py 的目錄就是一個套件。套件可以包含多個模組。

1.claude/lib/
2├── __init__.py      # 使 lib 成為套件
3├── git_utils.py     # 模組
4├── hook_io.py       # 模組
5├── hook_logging.py  # 模組
6└── config_loader.py # 模組

__init__.py 的作用

__init__.py 是套件的初始化檔案,它在套件被導入時執行。

實際範例:Hook 系統的 __init__.py

來自 .claude/lib/__init__.py

 1"""
 2Claude Hooks 共用程式庫
 3
 4提供 Hook 腳本共用的工具函式,消除程式碼重複。
 5
 6模組結構:
 7- git_utils: Git 操作工具(分支、worktree、專案根目錄)
 8- hook_logging: Hook 日誌系統
 9- hook_io: Hook 輸入輸出處理
10"""
11
12# 從子模組導入並重新匯出
13from .git_utils import (
14    run_git_command,
15    get_current_branch,
16    get_project_root,
17    get_worktree_list,
18    is_protected_branch,
19    is_allowed_branch,
20)
21
22from .hook_logging import setup_hook_logging
23
24from .hook_io import (
25    read_hook_input,
26    write_hook_output,
27    create_pretooluse_output,
28    create_posttooluse_output,
29)
30
31from .config_loader import (
32    load_config,
33    load_agents_config,
34    load_quality_rules,
35    clear_config_cache,
36)
37
38# 定義公開 API
39__all__ = [
40    # git_utils
41    "run_git_command",
42    "get_current_branch",
43    # ... 省略其他
44]
45
46__version__ = "0.28.0"

__init__.py 的三個主要功能

1. 宣告套件身份

空的 __init__.py 也能讓目錄成為套件:

1# lib/__init__.py(最簡形式)
2# 即使是空檔案,也使 lib 成為套件

2. 定義公開 API

透過 __all__ 列表控制 from package import * 的行為:

1__all__ = [
2    "run_git_command",
3    "get_current_branch",
4    "setup_hook_logging",
5]
6
7# 當使用者執行 from lib import * 時
8# 只會導入 __all__ 中列出的名稱

3. 簡化導入路徑

使用者可以直接從套件導入,而不需要知道子模組:

1# 有 __init__.py 的重新匯出:簡潔
2from lib import get_current_branch, setup_hook_logging
3
4# 沒有 __init__.py 的重新匯出:冗長
5from lib.git_utils import get_current_branch
6from lib.hook_logging import setup_hook_logging

相對導入與絕對導入

相對導入(使用 .

在套件內部使用相對導入:

1# 在 lib/config_loader.py 中
2from .git_utils import get_project_root  # 同級模組
3from . import hook_io                     # 導入整個模組

絕對導入

從套件外部或在腳本中使用絕對導入:

1# 在 .claude/hooks/some_hook.py 中
2import sys
3sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
4
5from git_utils import get_current_branch  # 絕對導入
6from hook_io import read_hook_input

套件版本管理

__init__.py 中定義版本號是常見做法:

1__version__ = "0.28.0"

這樣使用者可以查詢版本:

1import lib
2print(lib.__version__)  # "0.28.0"

模組載入順序

Python 搜尋模組的順序:

  1. 內建模組
  2. sys.path[0](腳本所在目錄)
  3. PYTHONPATH 環境變數
  4. 標準庫
  5. site-packages(第三方套件)

實際範例:Hook 腳本的路徑設定

 1#!/usr/bin/env python3
 2"""Branch Verify Hook"""
 3
 4import sys
 5from pathlib import Path
 6
 7# 將 lib 目錄加入搜尋路徑
 8sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
 9
10# 現在可以導入 lib 中的模組
11from git_utils import get_current_branch
12from hook_io import read_hook_input, write_hook_output

最佳實踐

1. 避免循環導入

 1# 不好:a.py 和 b.py 互相導入
 2# a.py
 3from b import func_b
 4# b.py
 5from a import func_a  # 循環導入!
 6
 7# 好:重構為第三個模組
 8# common.py
 9def shared_function(): ...
10# a.py
11from common import shared_function
12# b.py
13from common import shared_function

2. 延遲導入

對於可選依賴或避免循環導入:

1def load_yaml_config():
2    # 只在需要時才導入
3    try:
4        import yaml
5        return yaml.safe_load(...)
6    except ImportError:
7        # 備案方案
8        import json
9        return json.load(...)

3. 清晰的模組邊界

每個模組應該有單一、明確的職責:

1lib/
2├── git_utils.py      # Git 操作(單一職責)
3├── hook_io.py        # 輸入輸出處理(單一職責)
4├── hook_logging.py   # 日誌系統(單一職責)
5└── config_loader.py  # 配置載入(單一職責)

思考題

  1. 為什麼 __init__.py 使用 from .git_utils import ... 而不是 from git_utils import ...
  2. Hook 腳本中的 sys.path.insert(0, ...) 為什麼使用 0 作為索引?
  3. __all__ 列表的作用是什麼?如果不定義會怎樣?

實作練習

閱讀 .claude/lib/__init__.py,回答以下問題:

  1. 這個套件匯出了多少個公開函式?
  2. 套件的版本號是多少?
  3. 如果要新增一個 utils.py 模組並匯出 format_message 函式,需要修改哪些地方?

上一章:Python 哲學與設計理念 下一章:導入機制與路徑管理