5. 匯入系統¶
一個 模組 中的 Python 程式碼可以透過匯入過程來訪問另一個模組中的程式碼。 import
語句是呼叫匯入機制最常用的方式,但不是唯一的方式。 importlib.import_module()
和內建的 __import__()
等函式也可被用來呼叫匯入機制。
import
語句包含兩個操作;它會查詢指定的模組,然後將查詢結果繫結到當前作用域中的一個名稱上。 import
語句的查詢操作被定義為呼叫帶有適當引數的 __import__()
函式。 __import__()
的返回值被用於執行 import
語句的名稱繫結操作。 請參閱 import
語句瞭解其名稱繫結操作的確切細節。
對 __import__()
的直接呼叫只執行模組搜尋,如果找到,則執行模組建立操作。雖然可能會發生某些副作用,例如匯入父包以及更新各種快取(包括 sys.modules
),但只有 import
語句會執行名稱繫結操作。
當執行 import
語句時,會呼叫標準的內建 __import__()
函式。其他呼叫匯入系統的機制(例如 importlib.import_module()
)可能會選擇繞過 __import__()
並使用自己的解決方案來實現匯入語義。
當一個模組被首次匯入時,Python 會搜尋該模組,如果找到,它會建立一個模組物件[1] 並將其初始化。 如果找不到指定的模組,則會引發 ModuleNotFoundError
。 當匯入機制被呼叫時,Python 會應用多種策略來搜尋指定的模組。 這些策略可以透過使用下文各節中所描述的各種鉤子來加以修改和擴充套件。
在 3.3 版更改: 匯入系統已更新為完全實現 PEP 302 的第二階段。 不再有任何隱式的匯入機制 — 完整的匯入系統都透過 sys.meta_path
暴露出來。 此外,還實現了對原生名稱空間包的支援 (參見 PEP 420)。
5.1. importlib
¶
importlib
模組提供了與匯入系統互動的豐富 API。 例如 importlib.import_module()
為呼叫匯入機制提供了一個推薦的、比內建的 __import__()
更簡單的 API。 請參閱 importlib
庫文件瞭解更多細節。
5.2. 包¶
Python 只有一種模組物件型別,所有模組都屬於此型別,無論模組是用 Python、C 還是其他語言實現的。為了幫助組織模組並提供命名層次結構,Python 引入了 包 的概念。
你可以把包看作檔案系統中的目錄,而模組就是目錄中的檔案,但不要過於拘泥於這種類比,因為包和模組不一定非要來自檔案系統。在本篇文件中,我們將使用這種方便的目錄和檔案類比。與檔案系統目錄一樣,包是以分層結構組織的,包本身可能包含子包以及常規模組。
務必記住,所有包都是模組,但並非所有模組都是包。換句話說,包只是一種特殊的模組。具體來說,任何包含 __path__
屬性的模組都被視為包。
所有模組都有一個名稱。子包名稱與其父包名稱之間用點號分隔,類似於 Python 的標準屬性訪問語法。因此,你可能有一個名為 email
的包,它又有一個名為 email.mime
的子包,以及該子包中一個名為 email.mime.text
的模組。
5.2.1. 常規包¶
Python 定義了兩種型別的包:常規包 和 名稱空間包。常規包是 Python 3.2 及更早版本中存在的傳統包。常規包通常實現為一個包含 __init__.py
檔案的目錄。當匯入一個常規包時,這個 __init__.py
檔案會被隱式執行,它定義的物件會被繫結到該包的名稱空間中的名稱上。 __init__.py
檔案可以包含任何其他模組可以包含的 Python 程式碼,Python 在匯入該模組時會向其新增一些額外的屬性。
例如,以下檔案系統佈局定義了一個頂層 parent
包,它有三個子包:
parent/
__init__.py
one/
__init__.py
two/
__init__.py
three/
__init__.py
匯入 parent.one
將隱式地執行 parent/__init__.py
和 parent/one/__init__.py
。後續匯入 parent.two
或 parent.three
將分別執行 parent/two/__init__.py
和 parent/three/__init__.py
。
5.2.2. 名稱空間包¶
名稱空間包由多個 部分 組成,每個部分為父包提供一個子包。這些部分可能位於檔案系統的不同位置。部分也可能存在於 zip 檔案、網路上,或者 Python 在匯入期間搜尋的任何其他地方。名稱空間包可能直接對應於檔案系統上的物件,也可能不對應;它們可以是沒有任何具體表示的虛擬模組。
名稱空間包不使用普通列表作為其 __path__
屬性。它們使用一種自定義的可迭代型別,如果其父包的路徑(對於頂層包則是 sys.path
)發生變化,它將在該包內的下一次匯入嘗試時自動執行新的包部分搜尋。
對於名稱空間包,沒有 parent/__init__.py
檔案。事實上,在匯入搜尋過程中可能會找到多個 parent
目錄,每個目錄都由不同的部分提供。因此,parent/one
可能在物理上不與 parent/two
相鄰。在這種情況下,每當頂層 parent
包或其子包被匯入時,Python 都會為其建立一個名稱空間包。
另請參閱 PEP 420 瞭解名稱空間包的規範。
5.3. 搜尋¶
要開始搜尋,Python 需要被匯入模組的完全限定名稱(或包,但就本討論而言,兩者區別不大)。這個名稱可能來自 import
語句的各種引數,或者來自 importlib.import_module()
或 __import__()
函式的引數。
這個名稱將在匯入搜尋的各個階段使用,它可能是指向子模組的點分路徑,例如 foo.bar.baz
。在這種情況下,Python 首先嚐試匯入 foo
,然後是 foo.bar
,最後是 foo.bar.baz
。如果任何中間匯入失敗,則會引發 ModuleNotFoundError
。
5.3.1. 模組快取¶
在匯入搜尋過程中首先檢查的地方是 sys.modules
。這個對映充當了所有先前已匯入模組的快取,包括中間路徑。因此,如果之前匯入了 foo.bar.baz
,sys.modules
將包含 foo
、foo.bar
和 foo.bar.baz
的條目。每個鍵的值將是相應的模組物件。
在匯入期間,會在 sys.modules
中查詢模組名,如果存在,則關聯的值就是滿足匯入要求的模組,過程就此完成。但是,如果值為 None
,則會引發 ModuleNotFoundError
。如果模組名缺失,Python 將繼續搜尋該模組。
sys.modules
是可寫的。刪除一個鍵可能不會銷燬關聯的模組(因為其他模組可能持有對它的引用),但這會使該模組名的快取條目失效,導致 Python 在下次匯入時重新搜尋該模組。鍵也可以被賦值為 None
,從而強制下次匯入該模組時引發 ModuleNotFoundError
。
但請注意,如果你保留對模組物件的引用,使其在 sys.modules
中的快取條目失效,然後重新匯入該模組,這兩個模組物件將不是同一個。相比之下,importlib.reload()
會重用同一個模組物件,並透過重新執行模組程式碼來重新初始化模組內容。
5.3.2. 查詢器和載入器¶
如果在 sys.modules
中找不到指定的模組,Python 的匯入協議就會被呼叫以查詢和載入該模組。該協議由兩個概念性物件組成:查詢器和載入器。查詢器的任務是確定它是否能用其所知的任何策略找到指定的模組。同時實現這兩個介面的物件被稱為匯入器 —— 當它們發現自己可以載入所請求的模組時,它們會返回自身。
Python 包含許多預設的查詢器和匯入器。第一個知道如何定位內建模組,第二個知道如何定位凍結模組。第三個預設查詢器會搜尋匯入路徑以查詢模組。 匯入路徑是一個位置列表,可以是檔案系統路徑或 zip 檔案。它也可以擴充套件為搜尋任何可定位的資源,例如由 URL 標識的資源。
匯入機制是可擴充套件的,因此可以新增新的查詢器來擴充套件模組搜尋的範圍和領域。
查詢器實際上不載入模組。如果它們能找到指定的模組,它們會返回一個模組規格(module spec),這是模組匯入相關資訊的封裝,匯入機制在載入模組時會使用它。
以下各節將更詳細地描述查詢器和載入器的協議,包括如何建立和註冊新的查詢器和載入器以擴充套件匯入機制。
在 3.4 版更改: 在 Python 的早期版本中,查詢器直接返回載入器,而現在它們返回包含載入器的模組規格。載入器在匯入期間仍被使用,但職責減少了。
5.3.3. 匯入鉤子¶
匯入機制被設計為可擴充套件的;其主要機制是匯入鉤子。匯入鉤子有兩種型別:元鉤子和匯入路徑鉤子。
元鉤子在匯入處理開始時被呼叫,在除 sys.modules
快取查詢之外的任何其他匯入處理發生之前。這允許元鉤子覆蓋 sys.path
處理、凍結模組甚至內建模組。元鉤子透過向 sys.meta_path
新增新的查詢器物件來註冊,如下所述。
匯入路徑鉤子作為 sys.path
(或 package.__path__
)處理的一部分,在遇到其關聯的路徑項時被呼叫。匯入路徑鉤子透過向 sys.path_hooks
新增新的可呼叫物件來註冊,如下所述。
5.3.4. 元路徑¶
當在 sys.modules
中找不到指定的模組時,Python 接下來會搜尋 sys.meta_path
,它包含一個元路徑查詢器物件列表。這些查詢器按順序被查詢,以檢視它們是否知道如何處理指定的模組。元路徑查詢器必須實現一個名為 find_spec()
的方法,該方法接受三個引數:一個名稱、一個匯入路徑和(可選的)一個目標模組。元路徑查詢器可以使用它想要的任何策略來確定它是否可以處理指定的模組。
如果元路徑查詢器知道如何處理指定的模組,它會返回一個規格物件。如果它不能處理指定的模組,它會返回 None
。如果 sys.meta_path
處理到達其列表末尾而沒有返回規格,則會引發 ModuleNotFoundError
。引發的任何其他異常都會直接向上傳播,中止匯入過程。
元路徑查詢器的 find_spec()
方法被呼叫時帶有兩個或三個引數。第一個是被匯入模組的完全限定名稱,例如 foo.bar.baz
。第二個引數是用於模組搜尋的路徑條目。對於頂層模組,第二個引數是 None
,但對於子模組或子包,第二個引數是父包的 __path__
屬性的值。如果無法訪問相應的 __path__
屬性,則會引發 ModuleNotFoundError
。第三個引數是一個現有的模組物件,它將是後續載入的目標。匯入系統僅在過載期間傳入目標模組。
對於單個匯入請求,元路徑可能會被遍歷多次。例如,假設所涉及的模組都尚未被快取,匯入 foo.bar.baz
將首先執行一個頂層匯入,在每個元路徑查詢器(mpf
)上呼叫 mpf.find_spec("foo", None, None)
。在 foo
被匯入後,foo.bar
將透過第二次遍歷元路徑來匯入,呼叫 mpf.find_spec("foo.bar", foo.__path__, None)
。一旦 foo.bar
被匯入,最後的遍歷將呼叫 mpf.find_spec("foo.bar.baz", foo.bar.__path__, None)
。
一些元路徑查詢器只支援頂層匯入。當第二個引數傳入除 None
以外的任何值時,這些匯入器將總是返回 None
。
Python 預設的 sys.meta_path
有三個元路徑查詢器:一個知道如何匯入內建模組,一個知道如何匯入凍結模組,還有一個知道如何從匯入路徑中匯入模組(即基於路徑的查詢器)。
在 3.4 版更改: 元路徑查詢器的 find_spec()
方法取代了 find_module()
,後者現已棄用。雖然它將繼續工作,但匯入機制只有在查詢器未實現 find_spec()
時才會嘗試它。
在 3.10 版更改: 匯入系統使用 find_module()
現在會引發 ImportWarning
。
在 3.12 版更改: find_module()
已被移除。請改用 find_spec()
。
5.4. 載入¶
如果找到了一個模組規格,匯入機制將在載入模組時使用它(以及它包含的載入器)。以下是匯入過程中載入部分的大致過程:
module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
# It is assumed 'exec_module' will also be defined on the loader.
module = spec.loader.create_module(spec)
if module is None:
module = ModuleType(spec.name)
# The import-related module attributes get set here:
_init_module_attrs(spec, module)
if spec.loader is None:
# unsupported
raise ImportError
if spec.origin is None and spec.submodule_search_locations is not None:
# namespace package
sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
module = spec.loader.load_module(spec.name)
else:
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
except BaseException:
try:
del sys.modules[spec.name]
except KeyError:
pass
raise
return sys.modules[spec.name]
請注意以下細節:
如果
sys.modules
中已存在具有給定名稱的模組物件,import 將已經返回了它。在載入器執行模組程式碼之前,該模組就會存在於
sys.modules
中。這一點至關重要,因為模組程式碼可能會(直接或間接)匯入自身;事先將其新增到sys.modules
中可以防止最壞情況下的無限遞迴和最好情況下的多次載入。如果載入失敗,失敗的模組——且僅有失敗的模組——會從
sys.modules
中移除。任何已在sys.modules
快取中的模組,以及任何作為副作用成功載入的模組,都必須保留在快取中。這與過載(reloading)不同,在過載中,即使是失敗的模組也會留在sys.modules
中。在模組建立之後、執行之前,匯入機制會設定與匯入相關的模組屬性(在上面的虛擬碼示例中為“_init_module_attrs”),具體內容在後面的章節中總結。
模組執行是載入的關鍵時刻,模組的名稱空間在此期間被填充。執行完全委託給載入器,由載入器決定填充什麼內容以及如何填充。
在載入期間建立並傳遞給 exec_module() 的模組,可能不是在 import 結束時返回的那個模組[2]。
在 3.4 版更改: 匯入系統接管了載入器的模板職責。這些職責以前是由 importlib.abc.Loader.load_module()
方法執行的。
5.4.1. 載入器¶
模組載入器提供了載入的關鍵功能:模組執行。匯入機制呼叫 importlib.abc.Loader.exec_module()
方法,並傳入一個引數,即要執行的模組物件。從 exec_module()
返回的任何值都會被忽略。
載入器必須滿足以下要求:
如果模組是 Python 模組(而不是內建模組或動態載入的擴充套件),載入器應在模組的全域性名稱空間(
module.__dict__
)中執行模組的程式碼。如果載入器無法執行該模組,它應該引發一個
ImportError
,儘管在exec_module()
期間引發的任何其他異常都將被傳播。
在許多情況下,查詢器和載入器可以是同一個物件;在這種情況下,find_spec()
方法只需返回一個將載入器設定為 self
的規格。
模組載入器可以選擇透過實現 create_module()
方法來在載入期間建立模組物件。它接受一個引數,即模組規格,並返回在載入期間要使用的新模組物件。 create_module()
不需要對模組物件設定任何屬性。如果該方法返回 None
,匯入機制將自行建立新模組。
3.4 版後已移除: 載入器的 create_module()
方法。
在 3.4 版更改: load_module()
方法被 exec_module()
替代,匯入機制承擔了所有載入的模板職責。
為了與現有載入器相容,如果載入器的 load_module()
方法存在,並且該載入器沒有同時實現 exec_module()
,匯入機制將使用它。然而,load_module()
已被棄用,載入器應改為實現 exec_module()
。
load_module()
方法除了執行模組外,還必須實現上述所有模板載入功能。所有相同的約束都適用,並附加一些澄清:
如果
sys.modules
中存在具有給定名稱的模組物件,載入器必須使用該現有模組。(否則,importlib.reload()
將無法正常工作。)如果指定的模組在sys.modules
中不存在,載入器必須建立一個新的模組物件並將其新增到sys.modules
中。在載入器執行模組程式碼之前,該模組必須存在於
sys.modules
中,以防止無限遞迴或多次載入。如果載入失敗,載入器必須移除它已插入到
sys.modules
中的任何模組,但它必須僅移除失敗的模組,並且僅當載入器本身已顯式載入了該模組時。
在 3.5 版更改: 當定義了 exec_module()
但未定義 create_module()
時,會引發 DeprecationWarning
。
在 3.6 版更改: 當定義了 exec_module()
但未定義 create_module()
時,會引發 ImportError
。
在 3.10 版更改: 使用 load_module()
將引發 ImportWarning
。
5.4.2. 子模組¶
當使用任何機制(例如 importlib
API、import
或 import-from
語句,或內建的 __import__()
)載入子模組時,會在父模組的名稱空間中放置一個指向子模組物件的繫結。例如,如果包 spam
有一個子模組 foo
,在匯入 spam.foo
後,spam
將有一個屬性 foo
,它被繫結到子模組。假設你有以下目錄結構:
spam/
__init__.py
foo.py
並且 spam/__init__.py
中有以下這行程式碼:
from .foo import Foo
那麼執行以下程式碼會在 spam
模組中為 foo
和 Foo
建立名稱繫結:
>>> import spam
>>> spam.foo
<module 'spam.foo' from '/tmp/imports/spam/foo.py'>
>>> spam.Foo
<class 'spam.foo.Foo'>
考慮到 Python 熟悉的名稱繫結規則,這可能看起來令人驚訝,但它實際上是匯入系統的一個基本特性。不變的規則是,如果你有 sys.modules['spam']
和 sys.modules['spam.foo']
(就像在上述匯入後那樣),後者必須作為前者的 foo
屬性出現。
5.4.3. 模組規格¶
匯入機制在匯入期間,尤其是在載入之前,會使用關於每個模組的各種資訊。大部分資訊對所有模組都是通用的。模組規格的目的是將這些與匯入相關的資訊按模組封裝起來。
在匯入期間使用規格允許狀態在匯入系統元件之間傳遞,例如在建立模組規格的查詢器和執行它的載入器之間。最重要的是,它允許匯入機制執行載入的模板操作,而如果沒有模組規格,載入器就要承擔這個責任。
模組的規格透過 module.__spec__
暴露。適當地設定 __spec__
同樣適用於直譯器啟動期間初始化的模組。唯一的例外是 __main__
,在某些情況下,__spec__
會被設定為 None。
請參閱 ModuleSpec
瞭解模組規格內容的詳細資訊。
在 3.4 版本加入。
5.4.4. 模組的 __path__ 屬性¶
__path__
屬性應該是一個(可能為空的)字串序列,列出查詢包的子模組的位置。根據定義,如果一個模組有 __path__
屬性,它就是一個包。
一個包的 __path__
屬性在匯入其子包時使用。在匯入機制中,它的功能與 sys.path
非常相似,即提供一個在匯入期間搜尋模組的位置列表。然而,__path__
通常比 sys.path
更受限制。
用於 sys.path
的規則同樣適用於包的 __path__
。在遍歷包的 __path__
時,會諮詢 sys.path_hooks
(如下所述)。
一個包的 __init__.py
檔案可以設定或修改包的 __path__
屬性,這在 PEP 420 之前通常是實現名稱空間包的方式。隨著 PEP 420 的採用,名稱空間包不再需要提供僅包含 __path__
操作程式碼的 __init__.py
檔案;匯入機制會自動為名稱空間包正確設定 __path__
。
5.4.5. 模組的 repr¶
預設情況下,所有模組都有一個可用的 repr,但根據上面設定的屬性以及模組的規格,你可以更明確地控制模組物件的 repr。
如果模組有規格(__spec__
),匯入機制將嘗試從中生成一個 repr。如果失敗或沒有規格,匯入系統將使用模組上可用的任何資訊來構建一個預設的 repr。它將嘗試使用 module.__name__
、module.__file__
和 module.__loader__
作為 repr 的輸入,對於任何缺失的資訊都有預設值。
以下是使用的確切規則:
如果模組有
__spec__
屬性,規格中的資訊將用於生成 repr。會參考 “name”、“loader”、“origin” 和 “has_location” 屬性。如果模組有
__file__
屬性,它將被用作模組 repr 的一部分。如果模組沒有
__file__
但有一個不為None
的__loader__
,那麼載入器的 repr 將被用作模組 repr 的一部分。否則,只在 repr 中使用模組的
__name__
。
在 3.12 版更改: 自 Python 3.4 起已棄用的 module_repr()
,在 Python 3.12 中被移除,在解析模組的 repr 期間不再被呼叫。
5.4.6. 快取位元組碼的失效¶
在 Python 從 .pyc
檔案載入快取的位元組碼之前,它會檢查快取是否與源 .py
檔案保持最新。預設情況下,Python 透過在寫入快取檔案時儲存原始檔的最後修改時間戳和大小來實現。在執行時,匯入系統透過將快取檔案中儲存的元資料與原始檔的元資料進行比較來驗證快取檔案。
Python 還支援“基於雜湊”的快取檔案,它儲存原始檔內容的雜湊值,而不是其元資料。基於雜湊的 .pyc
檔案有兩種變體:已檢查和未檢查。對於已檢查的基於雜湊的 .pyc
檔案,Python 透過雜湊原始檔並將結果雜湊與快取檔案中的雜湊進行比較來驗證快取檔案。如果發現一個已檢查的基於雜湊的快取檔案無效,Python 會重新生成它並寫入一個新的已檢查的基於雜湊的快取檔案。對於未檢查的基於雜湊的 .pyc
檔案,Python 簡單地假設如果快取檔案存在,它就是有效的。基於雜湊的 .pyc
檔案驗證行為可以透過 --check-hash-based-pycs
標誌來覆蓋。
在 3.7 版更改: 添加了基於雜湊的 .pyc
檔案。以前,Python 只支援基於時間戳的位元組碼快取失效。
5.5. 基於路徑的查詢器¶
如前所述,Python 自帶幾個預設的元路徑查詢器。其中一個,稱為基於路徑的查詢器(PathFinder
),會搜尋一個匯入路徑,該路徑包含一個路徑條目列表。每個路徑條目都指定了一個搜尋模組的位置。
基於路徑的查詢器本身不知道如何匯入任何東西。相反,它會遍歷各個路徑條目,將每個條目與一個知道如何處理該特定型別路徑的路徑條目查詢器關聯起來。
預設的路徑條目查詢器集合實現了在檔案系統上查詢模組的所有語義,處理特殊的檔案型別,如 Python 原始碼(.py
檔案)、Python 位元組碼(.pyc
檔案)和共享庫(例如 .so
檔案)。當標準庫中的 zipimport
模組支援時,預設的路徑條目查詢器還處理從 zip 檔案載入所有這些檔案型別(共享庫除外)。
路徑條目不必侷限於檔案系統位置。它們可以指向 URL、資料庫查詢,或任何可以用字串指定的其他位置。
基於路徑的查詢器提供了額外的鉤子和協議,以便您可以擴充套件和自定義可搜尋路徑條目的型別。例如,如果您想支援將網路 URL 作為路徑條目,您可以編寫一個實現 HTTP 語義的鉤子,用於在 Web 上查詢模組。這個鉤子(一個可呼叫物件)將返回一個支援下述協議的路徑條目查詢器,然後用它從 Web 獲取模組的載入器。
警告:本節和前一節都使用了術語 *finder*,透過使用元路徑查詢器和路徑條目查詢器來區分它們。這兩種型別的查詢器非常相似,支援相似的協議,並在匯入過程中以相似的方式工作,但重要的是要記住它們有細微的差別。特別是,元路徑查詢器在匯入過程的開始階段操作,以 sys.meta_path
遍歷為觸發點。
相比之下,路徑條目查詢器在某種意義上是基於路徑的查詢器的實現細節,事實上,如果將基於路徑的查詢器從 sys.meta_path
中移除,任何路徑條目查詢器的語義都不會被呼叫。
5.5.1. 路徑條目查詢器¶
基於路徑的查詢器負責查詢和載入那些位置由字串路徑條目指定的 Python 模組和包。大多數路徑條目指定了檔案系統中的位置,但它們不必侷限於此。
作為元路徑查詢器,基於路徑的查詢器實現了之前描述的 find_spec()
協議,但它暴露了額外的鉤子,可用於自定義如何從匯入路徑中查詢和載入模組。
基於路徑的查詢器使用三個變數:sys.path
、sys.path_hooks
和 sys.path_importer_cache
。包物件上的 __path__
屬性也被使用。這些提供了自定義匯入機制的額外方式。
sys.path
包含一個字串列表,提供模組和包的搜尋位置。它從 PYTHONPATH
環境變數以及各種其他安裝和實現相關的預設值初始化。sys.path
中的條目可以命名檔案系統上的目錄、zip 檔案,以及可能應該搜尋模組的其他“位置”(參見 site
模組),例如 URL 或資料庫查詢。sys.path
中只應出現字串;所有其他資料型別都將被忽略。
基於路徑的查詢器是一個元路徑查詢器,因此匯入機制透過呼叫基於路徑的查詢器的 find_spec()
方法來開始匯入路徑搜尋,如前所述。當向 find_spec()
傳入 path
引數時,它將是一個要遍歷的字串路徑列表——通常是包內匯入時該包的 __path__
屬性。如果 path
引數為 None
,這表示一個頂層匯入,將使用 sys.path
。
基於路徑的查詢器會遍歷搜尋路徑中的每個條目,併為每個條目尋找一個合適的路徑條目查詢器(PathEntryFinder
)。因為這可能是一個昂貴的操作(例如,此搜尋可能涉及 stat()
呼叫的開銷),基於路徑的查詢器會維護一個將路徑條目對映到路徑條目查詢器的快取。這個快取維護在 sys.path_importer_cache
中(儘管名稱如此,這個快取實際上儲存的是查詢器物件,而不僅僅限於匯入器物件)。這樣,對特定路徑條目位置的路徑條目查詢器的昂貴搜尋只需執行一次。使用者程式碼可以自由地從 sys.path_importer_cache
中移除快取條目,從而強制基於路徑的查詢器再次執行路徑條目搜尋。
如果路徑條目不在快取中,基於路徑的查詢器會遍歷 sys.path_hooks
中的每個可呼叫物件。此列表中的每個路徑條目鉤子都會被呼叫,並傳入一個引數,即要搜尋的路徑條目。這個可呼叫物件可以返回一個能夠處理該路徑條目的路徑條目查詢器,也可以引發 ImportError
。基於路徑的查詢器使用 ImportError
來表示鉤子無法為該路徑條目找到路徑條目查詢器。該異常會被忽略,匯入路徑的迭代會繼續。鉤子應期望接收一個字串或位元組物件;位元組物件的編碼由鉤子決定(例如,它可能是檔案系統編碼、UTF-8 或其他),如果鉤子無法解碼該引數,它應該引發 ImportError
。
如果 sys.path_hooks
的迭代結束時沒有返回任何 路徑入口查詢器,那麼基於路徑的查詢器的 find_spec()
方法將在 sys.path_importer_cache
中存入 None
(表示該路徑入口沒有查詢器),並返回 None
,表明此 元路徑查詢器 無法找到該模組。
如果 sys.path_hooks
上的某個 路徑入口鉤子 可呼叫物件確實返回了一個 路徑入口查詢器,則會使用以下協議來請求查詢器提供一個模組 spec,這個 spec 之後會用於載入模組。
當前工作目錄(由一個空字串表示)的處理方式與 sys.path
上的其他條目略有不同。首先,如果無法確定當前工作目錄或發現其不存在,則不會在 sys.path_importer_cache
中儲存任何值。其次,每次查詢模組時,都會重新查詢當前工作目錄的值。第三,用於 sys.path_importer_cache
和由 importlib.machinery.PathFinder.find_spec()
返回的路徑將是實際的當前工作目錄,而不是空字串。
5.5.2. 路徑入口查詢器協議¶
為了支援匯入模組和已初始化的包,併為名稱空間包貢獻部分內容,路徑入口查詢器必須實現 find_spec()
方法。
find_spec()
接受兩個引數:要匯入的模組的完全限定名稱,以及(可選的)目標模組。find_spec()
返回一個該模組的完整 spec。這個 spec 的 "loader" 屬性總是會被設定(有一個例外)。
為了嚮導入機制表明 spec 代表一個名稱空間 部分,路徑入口查詢器需要將 submodule_search_locations
設定為一個包含該部分的列表。
在 3.4 版更改: find_spec()
取代了 find_loader()
和 find_module()
,這兩個方法現已棄用,但如果 find_spec()
未定義,仍會使用它們。
較舊的路徑入口查詢器可能會實現這兩個已棄用的方法之一,而不是 find_spec()
。為了向後相容,這些方法仍然受到支援。但是,如果在路徑入口查詢器上實現了 find_spec()
,則會忽略這些遺留方法。
find_loader()
接受一個引數,即要匯入的模組的完全限定名稱。find_loader()
返回一個二元組,其中第一項是載入器,第二項是名稱空間 部分。
為了與匯入協議的其他實現向後相容,許多路徑入口查詢器也支援元路徑查詢器所支援的、傳統的 find_module()
方法。然而,路徑入口查詢器的 find_module()
方法在呼叫時從不帶 path
引數(它們應從對路徑鉤子的初始呼叫中記錄適當的路徑資訊)。
路徑入口查詢器上的 find_module()
方法已被棄用,因為它不允許路徑入口查詢器為名稱空間包貢獻部分內容。如果路徑入口查詢器上同時存在 find_loader()
和 find_module()
,匯入系統將總是優先呼叫 find_loader()
。
在 3.10 版更改: 匯入系統呼叫 find_module()
和 find_loader()
將會引發 ImportWarning
。
在 3.12 版更改: find_module()
和 find_loader()
已被移除。
5.6. 替換標準的匯入系統¶
替換整個匯入系統最可靠的機制是刪除 sys.meta_path
的預設內容,並完全用自定義的元路徑鉤子替換它們。
如果只改變匯入語句的行為而不影響訪問匯入系統的其他 API 是可以接受的,那麼替換內建的 __import__()
函式可能就足夠了。這項技術也可以在模組級別使用,以僅改變該模組內匯入語句的行為。
要選擇性地阻止某些模組在元路徑早期被鉤子匯入(而不是完全停用標準匯入系統),從 find_spec()
中直接引發 ModuleNotFoundError
就足夠了,而不是返回 None
。後者表示元路徑搜尋應繼續,而引發異常會立即終止它。
5.7. 包的相對匯入¶
相對匯入使用前導點號。單個前導點號表示一個相對匯入,從當前包開始。兩個或更多的前導點號表示相對於當前包的父包的匯入,第一個點號之後的每個點號表示一個層級。例如,給定以下包佈局:
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py
在 subpackage1/moduleX.py
或 subpackage1/__init__.py
中,以下是有效的相對匯入:
from .moduleY import spam
from .moduleY import spam as ham
from . import moduleY
from ..subpackage1 import moduleY
from ..subpackage2.moduleZ import eggs
from ..moduleA import foo
絕對匯入可以使用 import <>
或 from <> import <>
語法,但相對匯入只能使用第二種形式;其原因是:
import XXX.YYY.ZZZ
應將 XXX.YYY.ZZZ
暴露為一個可用的表示式,但 .moduleY 不是一個有效的表示式。
5.8. __main__ 的特殊注意事項¶
相對於 Python 的匯入系統,__main__
模組是一個特例。正如在別處提到的,__main__
模組在直譯器啟動時直接初始化,很像 sys
和 builtins
。然而,與那兩者不同,它並不嚴格地算作一個內建模組。這是因為 __main__
的初始化方式取決於呼叫直譯器時使用的標誌和其他選項。
5.8.1. __main__.__spec__¶
根據 __main__
的初始化方式,__main__.__spec__
會被適當地設定,或者被設定為 None
。
當 Python 以 -m
選項啟動時,__spec__
會被設定為相應模組或包的模組 spec。__spec__
也會在 __main__
模組作為執行目錄、zip 檔案或其他 sys.path
條目的一部分被載入時被填充。
在其餘情況下,__main__.__spec__
被設定為 None
,因為用於填充 __main__
的程式碼並不直接對應一個可匯入的模組:
互動式提示符
-c
選項從 stdin 執行
直接從原始檔或位元組碼檔案執行
請注意,在最後一種情況下,__main__.__spec__
總是 None
,即使該檔案在技術上可以作為模組直接匯入。如果希望在 __main__
中獲得有效的模組元資料,請使用 -m
開關。
還要注意,即使當 __main__
對應一個可匯入的模組並且 __main__.__spec__
也相應地被設定時,它們仍然被認為是不同的模組。這是因為由 if __name__ == "__main__":
檢查保護的程式碼塊僅在模組用於填充 __main__
名稱空間時執行,而不是在正常匯入期間執行。
5.9. 參考文獻¶
自 Python 的早期以來,匯入機制已經有了相當大的發展。最初的包規範仍然可以閱讀,儘管自該文件編寫以來,一些細節已經發生了變化。
sys.meta_path
的最初規範是 PEP 302,後續在 PEP 420 中進行了擴充套件。
PEP 420 為 Python 3.3 引入了名稱空間包。PEP 420 還引入了 find_loader()
協議作為 find_module()
的替代方案。
PEP 366 描述了為主模組中的顯式相對匯入新增 __package__
屬性。
PEP 328 引入了絕對和顯式相對匯入,並最初為 __name__
提議了 PEP 366 最終為 __package__
指定的語義。
PEP 338 定義了將模組作為指令碼執行。
PEP 451 添加了將每個模組的匯入狀態封裝在 spec 物件中。它還將載入器的大部分樣板職責交還給了匯入機制。這些更改使得匯入系統中的幾個 API 被棄用,併為查詢器和載入器添加了新方法。
腳註