「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")

思考題

  1. 為什麼不把 lib 目錄加入 PYTHONPATH 環境變數,而要在每個腳本中設定 sys.path
  2. Path(__file__).resolve()Path(__file__) 有什麼區別?
  3. 如何驗證一個路徑是否已經在 sys.path 中?

實作練習

  1. 寫一個函式,列出 sys.path 中所有存在的目錄
  2. 建立一個簡單的模組,然後在另一個檔案中使用兩種不同的方式導入它

上一章:模組與套件組織 下一模組:型別系統