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 都會為該頂層 parent
包建立一個名稱空間包。
另請參閱 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 標識的資源。
匯入機制是可擴充套件的,因此可以新增新的查詢器來擴充套件模組搜尋的範圍和範圍。
查詢器實際上不載入模組。如果它們可以找到命名模組,則它們會返回一個 模組規範,它是模組匯入相關資訊的封裝,匯入機制在載入模組時使用該規範。
以下各節將更詳細地描述查詢器和載入器的協議,包括如何建立和註冊新的查詢器和載入器以擴充套件匯入機制。
在 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
中存在具有給定名稱的現有模組物件,則匯入將已返回它。模組將存在於
sys.modules
中,然後再由載入器執行模組程式碼。這至關重要,因為模組程式碼可能會(直接或間接)匯入自身;預先將其新增到sys.modules
可防止在最壞情況下出現無限遞迴,並在最佳情況下防止多次載入。如果載入失敗,則失敗的模組(且僅失敗的模組)將從
sys.modules
中刪除。已經存在於sys.modules
快取中的任何模組,以及作為副作用成功載入的任何模組,都必須保留在快取中。這與重新載入形成對比,在重新載入中,即使是失敗的模組也會留在sys.modules
中。在建立模組之後但在執行之前,匯入機制會設定與匯入相關的模組屬性(上面虛擬碼示例中的“_init_module_attrs”),如後面的章節中所述。
模組執行是載入的關鍵時刻,此時模組的名稱空間將被填充。執行完全委託給載入器,載入器可以決定填充的內容和方式。
在載入期間建立並傳遞給 exec_module() 的模組可能不是在匯入結束時返回的模組 [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()
方法。但是,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__
,但確實有__loader__
且不為None
,則將載入器的 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 只需假定快取檔案有效。可以使用 --check-hash-based-pycs
標誌覆蓋基於雜湊的 .pyc
檔案驗證行為。
在 3.7 版本中變更: 添加了基於雜湊的 .pyc
檔案。以前,Python 僅支援基於時間戳的位元組碼快取失效。
5.5. 基於路徑的查詢器¶
正如前面提到的,Python 自帶幾個預設的元路徑查詢器。其中一個稱為基於路徑的查詢器 (PathFinder
),它搜尋一個匯入路徑,其中包含路徑條目的列表。每個路徑條目指定一個用於搜尋模組的位置。
基於路徑的查詢器本身並不知道如何匯入任何內容。相反,它會遍歷各個路徑條目,將每個路徑條目與一個路徑條目查詢器關聯起來,該查詢器知道如何處理特定型別的路徑。
預設的路徑條目查詢器集合實現了在檔案系統上查詢模組的所有語義,處理諸如 Python 原始碼(.py
檔案)、Python 位元組碼(.pyc
檔案)和共享庫(例如 .so
檔案)等特殊檔案型別。當標準庫中的 zipimport
模組支援時,預設的路徑條目查詢器還可以處理從 zip 檔案載入所有這些檔案型別(共享庫除外)。
路徑條目不必侷限於檔案系統位置。它們可以引用 URL、資料庫查詢或任何其他可以指定為字串的位置。
基於路徑的查詢器提供了額外的鉤子和協議,以便您可以擴充套件和自定義可搜尋的路徑條目的型別。例如,如果您想支援將路徑條目作為網路 URL,您可以編寫一個實現 HTTP 語義的鉤子,以在 Web 上查詢模組。這個鉤子(一個可呼叫物件)將返回一個路徑條目查詢器,它支援下面描述的協議,然後該查詢器用於從 Web 獲取模組的載入器。
一個警告:本節和上一節都使用了術語查詢器,並透過使用術語元路徑查詢器和路徑條目查詢器來區分它們。這兩種型別的查詢器非常相似,支援類似的協議,並在匯入過程中以類似的方式運作,但重要的是要記住它們之間存在細微的差異。特別是,元路徑查詢器在匯入過程的開始階段執行,透過 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()
方法將 None
儲存在 sys.path_importer_cache
中(以指示此路徑條目沒有查詢器),並返回 None
,表明此元路徑查詢器無法找到該模組。
如果 sys.path_hooks
上的可呼叫物件之一返回了路徑條目查詢器,則使用以下協議向查詢器請求模組規範,該規範在載入模組時使用。
當前工作目錄(用空字串表示)的處理方式與其他 sys.path
中的條目略有不同。首先,如果發現當前工作目錄不存在,則不會在 sys.path_importer_cache
中儲存任何值。其次,每次查詢模組時都會重新查詢當前工作目錄的值。第三,用於 sys.path_importer_cache
的路徑,以及由 importlib.machinery.PathFinder.find_spec()
返回的路徑將是實際的當前工作目錄,而不是空字串。
5.5.2. 路徑條目查詢器協議¶
為了支援模組和已初始化包的匯入,併為名稱空間包貢獻部分內容,路徑條目查詢器必須實現 find_spec()
方法。
find_spec()
接受兩個引數:要匯入的模組的完全限定名稱,以及(可選的)目標模組。 find_spec()
返回模組的完整填充的規範。此規範將始終設定“loader”(有一個例外)。
為了嚮導入機制指示該規範表示一個名稱空間 部分,路徑條目查詢器將 submodule_search_locations
設定為包含該部分的列表。
在 3.4 版本中變更: find_spec()
取代了 find_loader()
和 find_module()
,兩者現在都已棄用,但如果未定義 find_spec()
,則仍將使用它們。
較舊的路徑條目查詢器可能會實現這兩個已棄用的方法之一,而不是 find_spec()
。 為了向後相容,這些方法仍然受到尊重。但是,如果在路徑條目查詢器上實現了 find_spec()
,則會忽略傳統方法。
find_loader()
接受一個引數,即要匯入的模組的完全限定名稱。 find_loader()
返回一個 2 元組,其中第一項是載入器,第二項是名稱空間 部分。
為了與其他匯入協議的實現向後相容,許多路徑條目查詢器還支援與元路徑查詢器相同的傳統 find_module()
方法。但是,路徑條目查詢器 find_module()
方法永遠不會使用 path
引數呼叫(它們應該從對路徑鉤子的初始呼叫中記錄相應的路徑資訊)。
路徑條目查詢器上的 find_module()
方法已棄用,因為它不允許路徑條目查詢器為名稱空間包貢獻部分內容。 如果路徑條目查詢器上同時存在 find_loader()
和 find_module()
,則匯入系統將始終優先呼叫 find_loader()
而不是 find_module()
。
在 3.10 版本中變更: 匯入系統對 find_module()
和 find_loader()
的呼叫將引發 ImportWarning
。
在 3.12 版本中變更: find_module()
和 find_loader()
已被刪除。
5.6. 替換標準匯入系統¶
替換整個匯入系統最可靠的機制是刪除 sys.meta_path
的預設內容,並完全替換為自定義的元路徑鉤子。
如果只改變 import 語句的行為,而不影響其他訪問匯入系統的 API 是可以接受的,那麼替換內建的 __import__()
函式可能就足夠了。 此技術也可以在模組級別使用,以僅更改該模組中 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__ 的特殊注意事項¶
__main__
模組是相對於 Python 匯入系統的一種特殊情況。正如 其他地方 所述,__main__
模組在直譯器啟動時直接初始化,很像 sys
和 builtins
。但是,與這兩個模組不同,它不嚴格符合內建模組的條件。這是因為 __main__
的初始化方式取決於呼叫直譯器時使用的標誌和其他選項。
5.8.1. __main__.__spec__¶
根據 __main__
的初始化方式,__main__.__spec__
會被適當地設定或設定為 None
。
當 Python 使用 -m
選項啟動時,__spec__
將設定為相應模組或包的模組規範。 當 __main__
模組作為執行目錄、zip 檔案或其他 sys.path
條目的一部分載入時,也會填充 __spec__
。
在 其餘情況 中,__main__.__spec__
設定為 None
,因為用於填充 __main__
的程式碼與可匯入的模組不直接對應。
互動式提示
-c
選項從標準輸入執行
直接從原始碼或位元組碼檔案執行
請注意,在最後一種情況下,__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 在規範物件中增加了每個模組匯入狀態的封裝。它還將載入器的大部分樣板職責轉移回了匯入機制。 這些更改允許棄用匯入系統中的幾個 API,並向查詢器和載入器新增新方法。
腳註