Python Package Import 之痛

SolomonXie發表於2019-02-16

參考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__裡規定的模組。

__init____path__ 多層次目錄匯入

參考Python官方:Packages in Multiple Directories

相關文章