參考Python官方:Packages
▶參考:Python相對匯入一處不解
參考:使用相對路徑名匯入包中子模組
理解Package
Python裡,就像所有的.py
檔案被稱為Module
模組一樣,所有的資料夾都被稱為Package
包。前提是,這個資料夾裡有一個__init__.py
檔案,可以是空檔案也可以有一些方便都內容。
一旦一個資料夾可以被視為Package
,那麼其中的所有檔案都會有獨立的Namespace名稱空間,即變數都不共享,與其它的package完全獨立。一個專案裡,可以有很多個子資料夾、子子資料夾,一旦變成package,那麼它們都能互相獨立,方便我們引用。
理解sys.path
Python的目錄結構中,“向當前指令碼以下的目錄import”永遠不會出問題,出問題的,總是“向上一層目錄import”
在Python的執行一個.py
指令碼的時候,會自動將sys.path
設定為指令碼所在目錄。然後凡是這個sys.path
之下的所有的檔案模組,都能直接import匯入。
但是,很明顯的,只有這個.py
指令碼之內的目錄才包括在path裡,也就是說它之外的所有事情它都一無所知。相當於Linux shell中的PATH。
在Python的匯入邏輯裡,非常/非常/非常重要的一點必須時時刻刻記在心上:
啟動指令碼所在的目錄,將持續作為當前目錄
來執行所有匯入的模組。
也就是說,如果你要匯入另一資料夾的模組,而那個模組依賴於它同級目錄的一個模組,這個時候它是不能直接匯入同級目錄模組的!因為當前的path已經變了!
比如,目錄結構如下:
Project
├── __init__.py
├── main.py
├── sub1
│ ├── __init__.py
│ ├── common1.py
│ ├── mod1.py *
└── sub2
├── __init__.py
├── common2.py
└── mod2.py *
假設,我們在mod1.py
中使用from common1 import *
來引用同級目錄的sub1/common1.py
。
同時,我們在mod2.py
中使用from common2 import *
來引用同級目錄的sub2/common2.py
。
另外,我們在mod1.py
中需要匯入mod2.py
。(先略過具體實現方法)
那麼問題出現了:
當我用$ python mod1.py
時候,當前的path是project/sub1/
,匯入mod2.py
後,它在執行from common2 ...
的時候,理所當然的是找不到的,因為sub1中沒有common2.py這個檔案!
理解Python模組的相對引用和絕對引用
那麼現在,我們應該做什麼呢?改變mod2.py
中的匯入路徑嗎?
當然不行!我們不能隨便改一個Package的匯入邏輯,如果這個package是別的作者寫的怎麼辦?如果改了以後,那個package以自身為入口
時候執行怎麼辦?這些全都會亂套。
所以,解決方案是:
強制要求從整個專案的頂層用
python -m project.sub1.mod1
來設定端正的PATH路徑。 然後其它所有子模組、子包都用from project.sub2 import mod2
這樣的完整專案引用的語句來匯入。
這個做法是PEP官方推薦的,也是合邏輯的,即:
一個完整的專案執行就應當以專案為入口來執行所有的子module
或子package
。
如果又想讓某個子package被同專案的其它子package引用,又想單獨執行,那就應當徹底把它從資料夾裡抽離出來變成一個單獨的“第三方庫”來用。
那麼具體應該怎麼修改各個檔案中的匯入語句呢:
- 刪除每個檔案中的相對引用,如
from common1 import *
。 - 改為專案級別的絕對引用,如:
from project.sub2 import mod2
- 如果需要執行/測試某個子模組的檔案,需要cd切換到專案目錄的再上一層中,執行:
python -m project.sub1.mod1
。注意,這裡的命名是包/模組式的,不能以.py
結尾!
Sibling Package Imports 父目錄中的同級目錄匯入
具體問題來了:怎麼匯入父級目錄中的其它模組或包呢?
再複習一下我們的目錄結構:
Project
├── main.py
├── sub1
│ ├── common.py
│ ├── mod1.py *
└── sub2
├── common.py
└── mod2.py *
目的:在sub1/mod1.py
中,匯入sub2/mod2.py
。同時,mod2.py
中還需要匯入同目錄的common.py
才能工作。
這是一個專案裡再正常不過的操作了,可是如果你嘗試google一下的話,可能會花費你數小時,結果還是讓你自己的大腦Stack-overflow.
目前stackoverflow會有人提供這幾種hacks來實現我們的目的:
- 在
mod1.py
中用sys.path.append(`..`)
來把父級目錄加到path中引用 - 用
from ..sub2 import mod2
來實現相對引用 - 直接
from project.sub2 import mod2
- 在
__init__.py
進行一些path設定或import匯入。
經過不斷的實踐,發現他們大都沒說清楚上下文,甚至沒有告訴完整的解決方案。
總結出的經驗就是:以上的那些hacks終究是hacks,不是官方推薦的,也不能真正派上用場。
比如sys.path.append()
方法,一開始似乎走通了不報錯。但是真實專案走起來,比不可能給每個檔案都加一句sys.path.append()
。走到最後,你用print( sys.path )
會發現path中莫名其妙多了很多很多路徑。這肯定不行,也沒見過任何專案原始碼裡這麼寫的。
再比如from ..sub2 import *
這樣的相對引用,能這麼用的上下文必須是:執行指令碼時要是從資料夾頂級入口執行,如果直接從mod1.py
執行指令碼,那麼這句話是會報錯的。
所以還是用官方推薦的方法和邏輯,丟棄那些hacks吧。
要達到sibling imports
,在這個例子裡,具體做法是:
- 修改所有的模組mod1.py和mod2.py,把匯入語句改為
from project.sub? import mod?
這樣的。 - 如果要匯入具體某個模組中的類或函式,則:
from project.sub?.mod? import MyClass
- 執行子包中的某個子模組時,cd到project目錄再往上一層,輸入
python -m project.sub1.mod1
執行 - 執行整個專案的話,就
python -m project
__init__
和 __all__
限制匯入模組
參考Python官方: Importing ✱ From a Package
Python規定:
如果在一個package包中的__init__.py
中寫上__all__ = [`模組1`, `模組2`, `模組3`]
的話,
那麼在其它模組引用這個package包使用from PACKAGE import *
這種用法的時候,
就不會真的引用包中所有的模組(那樣會很耗記憶體),而只能匯入作者在__all__
裡規定的模組。