最近因為資料分析開始大量使用 Python,與同事協作在同一個 Package 下拆分不同 Module 實作,當我想直接執行 Module 遇上了 「ModuleNotFoundError: No module named」的錯誤,讓我開始想瞭解 Python 的 Package 系統運作的原理
Python Package 載入方式
官方文件:6. 模組 (Module) 寫得很詳盡,模組的載入順序是根據 sys.path
,而 sys.path 依序由以下組成
- 當前路徑
- 環境變數 PYTHONPATH
- site-package
其中 site-package 是使用手動建置 package 所在位置 (pip install, python setup.py install
),可能會有非常多組,每一位 user / venv 下又有對應的 site-package,參考 How do I find the location of my Python site-packages directory?可以找出對應的路徑
- Global:
$ python -m site
- User:
$ python -m site --user-site
所以 sys.path 決定了 package 載入的順序,例如當前路徑下 package 名稱與核心模組重複,那會以當前路徑優先載入;
所以能透過動態改變 sys.path 決定載入順序
為什麼直接執行 Package 下的 Module 會失敗
在 Python 中,import 可以選擇完整的 Module 路徑或是相對路徑,而路徑會受到執行檔案的 sys.path 與 __package__ 的影響,相關的 magic method 為
__name__
:Module 的完整名稱__package__
:決定相對路徑 import 的解析路徑,如果檔案是 Package,則 __package__ 會等於 __name__;
如果是 Module 則 __package__ 是所屬的 Package 名稱;
如果 Module 是top-level modules
,也就是__name__ == __main__
,則 __package__ 為 None
以下參考自 Relative imports in Python 3,假設目前的專案目錄是
|
|
在 myothermodule.py 中,載入 mymodule,可以透過完整路徑 import mypackage.mymodule
或是相對路徑 import .mymodule
的方式,但如果直接執行 $python myothermodule 會分別遇到以下錯誤
- 完整路徑
|
|
- 相對路徑
|
|
完整路徑的錯誤原因是因為 sys.path 中沒有 mypackage,sys.path 是加入script 當前的資料夾 (./mypackage)
而不是 ./,所以 mypackage 是無法被載入的;
相對路徑的錯誤則是因為當前 myothermodule.py 是 top-level module, __package__ 被設定為 None,所以相對路徑解析會失敗
如何解決
分別針對完整路徑與相對路徑提出解決方案
1. 完整路徑
既然完整路徑是因為 sys.path 沒有包含到 package 的上層路徑而沒有被載入,那就加上去即可
Python 鼓勵但不強制 import 都要放在檔案開頭
|
|
直接安裝 package
另一個作法是透過 setuptools 直接安裝相依的套件,這樣就能從 site-package import,但這改動相對麻煩些,且變成套件要額外管理反而麻煩
2. 相對路徑
使用 -m 執行
直接指定完整的 package.module 路徑 $ python -m mypackage.myothermodule
,此時 __package__ 會被正確解析成 mypackage,參考 PEP366
By adding a new module level attribute, this PEP allows relative imports to work automatically if the module is executed using the -m switch
手動指定
手動實作 PEP366 的提案,參考PEP366_boilerplate.py,加入對應的 sys.path 並指定 __package__,達到跟 -m 相同的效果
|
|
結語
第一次接觸 Python 覺得頗神奇,有許多 magic number 以及獨樹一格的 package 管理方式,包含 virtual env 的使用等