1.4 導入機制與路徑管理
1.4 導入機制與路徑管理為什麼用
避免
「ModuleNotFoundError」是 Python 開發者最常遇到的錯誤之一。理解導入機制可以幫助你快速解決這類問題。
承接提示:import 問題通常是 script、module、package 與執行位置共同造成的結果,而非單一語法問題。如果還不確定這幾個概念的差異,請先閱讀 從單一 script 到多檔案專案。
模組搜尋路徑
Python 使用 sys.path 列表來搜尋模組。你可以查看當前的搜尋路徑:
1import sys
2for path in sys.path:
3 print(path)典型輸出:
1/current/script/directory # 腳本所在目錄
2/usr/local/lib/python3.11 # 標準庫
3/usr/local/lib/python3.11/site-packages # 第三方套件Hook 腳本的導入問題
問題情境
Hook 腳本位於 .claude/hooks/,共用模組位於 .claude/lib/:
1.claude/
2├── hooks/
3│ └── branch-verify-hook.py # 需要導入 lib 的模組
4└── lib/
5 ├── __init__.py
6 └── git_utils.py如果直接在 Hook 腳本中寫 from git_utils import ...,會得到 ModuleNotFoundError。
解決方案
在導入前將 lib 目錄加入搜尋路徑:
1#!/usr/bin/env python3
2"""Branch Verify Hook"""
3
4import sys
5from pathlib import Path
6
7# 計算 lib 目錄的路徑
8# __file__ = .claude/hooks/branch-verify-hook.py
9# parent = .claude/hooks/
10# parent.parent = .claude/
11# parent.parent / "lib" = .claude/lib/
12lib_path = Path(__file__).parent.parent / "lib"
13
14# 插入到搜尋路徑的最前面
15sys.path.insert(0, str(lib_path))
16
17# 現在可以導入了
18from git_utils import get_current_branch
19from hook_io import read_hook_input為什麼用 insert(0, ...) 而不是 append(...)?
1# 優先搜尋我們的模組(推薦)
2sys.path.insert(0, str(lib_path))
3
4# 最後才搜尋我們的模組(可能被標準庫覆蓋)
5sys.path.append(str(lib_path))如果你的模組名稱與標準庫衝突(例如 email.py),使用 append 會導致導入標準庫而非你的模組。
使用 Path 物件
Path 的基本操作
1from pathlib import Path
2
3# 取得目前檔案的路徑
4current_file = Path(__file__)
5
6# 取得父目錄
7parent_dir = current_file.parent
8
9# 組合路徑
10lib_path = parent_dir / "lib" # 使用 / 運算子
11
12# 轉換為字串
13lib_path_str = str(lib_path)路徑解析的陷阱
1# __file__ 可能是相對路徑
2print(__file__) # 可能是 "./hooks/my_hook.py"
3
4# 轉換為絕對路徑
5absolute_path = Path(__file__).resolve()
6print(absolute_path) # "/home/user/project/.claude/hooks/my_hook.py"環境變數方式
另一種方法是使用 PYTHONPATH 環境變數:
1# 在 shell 中設定
2export PYTHONPATH="${PYTHONPATH}:/path/to/.claude/lib"
3
4# 然後執行腳本
5python .claude/hooks/my_hook.py專案根目錄的取得
Hook 系統中經常需要取得專案根目錄:
1def get_project_root() -> str:
2 """
3 獲取專案根目錄(git 倉庫根目錄)
4
5 Returns:
6 str: 專案根目錄路徑
7 """
8 success, output = run_git_command(["rev-parse", "--show-toplevel"])
9 return output if success else os.getcwd()使用範例:
1root = get_project_root()
2config_path = os.path.join(root, ".claude", "config.json")常見錯誤與解決
錯誤 1: ModuleNotFoundError
1ModuleNotFoundError: No module named 'git_utils'解決方案:確認 sys.path.insert() 在導入語句之前。
錯誤 2: 相對導入錯誤
1ImportError: attempted relative import with no known parent package原因:在腳本中使用相對導入。
解決方案:在腳本中使用絕對導入,相對導入只用於套件內部。
1# 錯誤:在腳本中使用相對導入
2from .lib import git_utils
3
4# 正確:使用絕對導入
5from lib import git_utils錯誤 3: 循環導入
1ImportError: cannot import name 'xxx' from partially initialized module解決方案:重構程式碼,避免模組間互相依賴。
導入風格指南
導入順序(PEP 8)
1# 1. 標準庫
2import os
3import sys
4from pathlib import Path
5
6# 2. 第三方套件
7import yaml
8import requests
9
10# 3. 本地模組
11from lib.git_utils import get_current_branch
12from lib.hook_io import read_hook_input避免 import *
1# 不推薦
2from lib.git_utils import *
3
4# 推薦
5from lib.git_utils import (
6 get_current_branch,
7 get_project_root,
8 is_protected_branch,
9)長導入的換行
1from lib.git_utils import (
2 run_git_command,
3 get_current_branch,
4 get_project_root,
5 get_worktree_list,
6 is_protected_branch,
7 is_allowed_branch,
8)實際範例:完整的 Hook 腳本開頭
1#!/usr/bin/env python3
2"""
3Branch Verify Hook
4
5驗證當前分支是否適合進行編輯操作。
6"""
7
8# ===== 標準庫導入 =====
9import os
10import sys
11from pathlib import Path
12
13# ===== 路徑設定 =====
14# 將 lib 目錄加入搜尋路徑
15sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
16
17# ===== 本地模組導入 =====
18from git_utils import get_current_branch, is_protected_branch
19from hook_io import read_hook_input, write_hook_output, create_pretooluse_output
20from hook_logging import setup_hook_logging
21
22# ===== 初始化 =====
23logger = setup_hook_logging("branch-verify")思考題
- 為什麼不把 lib 目錄加入
PYTHONPATH環境變數,而要在每個腳本中設定sys.path? Path(__file__).resolve()和Path(__file__)有什麼區別?- 如何驗證一個路徑是否已經在
sys.path中?
實作練習
- 寫一個函式,列出
sys.path中所有存在的目錄 - 建立一個簡單的模組,然後在另一個檔案中使用兩種不同的方式導入它