Python名稱空間包

2020-07-16 10:05:05
提到名稱空間,其實包含 2 層含義,一種是語言上下文的名稱空間,其實在編寫 Python 程式時,無時無刻都在使用名稱空間,比如模組使用的全域性名稱空間、函數或方法呼叫的本地名稱空間等;另一種名稱空間可以在包的層面提供,稱為名稱空間包

簡單的理解,名稱空間包是對相關的包或模組進行分組的一種方法,通常是一個被忽略的功能,它對於在較大的專案中構建打包生態系統非常有用。

如果你的應用元件的開發、打包和版本化都是獨立的,但仍然希望從同一個名稱空間存取它們,那麼名稱空間包特別有用,它有利於明確每個包所屬的組織或專案。

例如,假設有一個 Acme 公司,該公司中使用共同的 acme 名稱空間,同時建立通用的 acme 名稱空間包作為該組織的其他包的容器。如果 Acme 公司中的某人想要向這個名稱空間貢獻一個與 SQL 相關的庫,那麼他需要在 acme 中註冊自己新的 acme.sql 包,整個檔案結構如下所示:
$tree acme/
acme/
├───acme
│      ├───__init__.py
│      └───sql
│              └──__init__.py
└───setup.py

2 directories, 3 files
在此基礎上,如果想新增一個新的子包,例如新增 templating,則需要將其包含在 acme 的原始碼樹中,如下所示:
$tree acme/
acme/
├───acme
│      ├───__init__.py
│      ├───sql
│      │      └──__init__.py
│      └───templating
│              └──__init__.py
└───setup.py

3 directories, 4 files
仔細觀察就會發現,採用這種方式幾乎不可能單獨開發 acme.sql 和 acme.templating。且 setup.py 指令碼還必須指定每個子包的所有依賴,所以不可能(至少非常困難)選擇性地安裝 acme 中的部分元件。此外,如果某些子包的需求檔案有衝突,是一個無法解決的問題。

通過利用名稱空間包,我們可以單獨儲存每個子包的原始碼樹,如下所示:
$tree acme.sql/
acme.sql/
├───acme
│      └───sql
│              └──__init__.py
└───setup.py

2 directories, 2 files

$tree acme.templating/
acme.templating/
├───acme
│      └───templating
│                 └──__init__.py
└───setup.py

2 directories, 2 files
由此,就可以在 PyPI 或者使用的任何包索引中單獨註冊它們,使用者還可以從 acme 名稱空間中選擇想要安裝的子包,而無需安裝通用的 acme 包,執行程式碼如下:

$pip install acme.sql acme.templating

注意,獨立的原始碼樹不足以在 Python 中建立名稱空間包,如果不想讓包之間相互覆蓋,就需要做一些額外的工作,此外,正確的處理方式也會隨著 Python 版本的不同而有所不同。

隱式名稱空間包

如果你只使用 Python 3.x,也只面向 Python 3.x 的使用者,則可以使用 PEP 420 引入的定義命令空間的新方法,即隱式名稱空間包。它是標準路徑的一部分,並從 Python 3.3 版本開始成為語言官方內容的一部分。

簡單來說,對於每一個包含 Python 包或模組(也包括名稱空間包)的目錄來說,如果其不包含 __init__().py 檔案,那麼它就被看做是名稱空間包。

例如,前面所說的 acme 在 Python 3.3 以及更高版本中,就是一個名稱空間包。使用安裝工具的最小 setup.py 指令碼檔案如下所示:
from setuptools import setup

setup(
    name = 'acme.templating',
    packages = ['acme.templating'],
)
但是,直到發表本節時,setuptools.find_packages() 還不支援 PEP 420,但這在未來很可能會改變。此外,要想實現名稱空間包的簡單繼承,顯示地定義包列表是值得的。

以前Python版本中的名稱空間包

Python 3.3 之前的版本中,雖無法使用 PEP 420 布局中的名稱空間包,但仍可以使用它。舊版 Python 中,有幾種方法可以將包定義成名稱空間。

最簡單的方法就是為每個元件建立一個檔案結構,類似於沒有名稱空間包的普通包布局,並將所有事情都留給 setuptools。

因此,acme.sql 和 acme.templating 的佈局範例可能如下所示:
$tree acme.sql/
acme.sql/
├───acme
│      ├──__init__.py
│      └───sql
│              └──__init__.py
└───setup.py

2 directories, 3 files

$tree acme.templating/
acme.templating/
├───acme
│      ├──__init__.py
│      └───templating
│                └──__init__.py
└───setup.py

2 directories, 3 files
注意,acme.sql 和 acme.templating 都有一個額外的原始碼檔案 acme/__init__.py,這個檔案必須是空的。

如果我們提供 acme 作為 setuptools.setup() 函數 namespace_package 關鍵字引數的值,那麼將會建立如下的 acme 名稱空間包:
from setuptools import setup

setup(
    name = 'acme.templating',
    packages = ['acme.templating'],
    namespace_package = ['acme'],
)
當然,最簡單的方法不一定是最好的,為了註冊一個新的名稱空間,setuptools 將會在 __init__.py 檔案中呼叫 pkg_resources.declare_namespace() 函數,即便 __init__.py 檔案是空的也會呼叫。

無論如何,正如官方文件所說,你自己負責在 __init__.py 檔案中宣告名稱空間,並且未來可能會刪除 setuptools 的這個隱式行為。為了保證安全,也為了未來依然可用(future-proof),需要將下面這行程式碼新增到 acme/__init__.py 檔案中:

__import__('pkg_resources').declare_namespace(__name__)