5.模組引入系統¶
一個module 中的 Python 程式碼透過importing 的過程來存取另一個模組中的程式碼。import 陳述式是叫用 (invoke) 引入機制最常見的方法,但這不是唯一的方法。函式如importlib.import_module() 以及內建函式__import__() 也可以用來叫用引入機制。
import 陳述式結合了兩個操作:首先搜尋指定的模組,然後將搜尋結果繫結到本地作用域中的一個名稱。import 陳述式的搜尋操作被定義為一個對__import__() 函式的呼叫,並帶有相應的引數。__import__() 的回傳值用於執行import 陳述式的名稱繫結操作。有關名稱繫結操作的詳細資訊,請參見import 陳述式。
直接呼叫__import__() 只會執行模組搜尋操作,以及在找到時執行模組的建立操作。雖然某些副作用可能會發生,例如引入父套件 (parent package),以及更新各種快取(包括sys.modules),但只有import 陳述式會執行名稱繫結操作。
當執行import 陳述式時,會呼叫內建的__import__() 函式。其他叫用引入系統的機制(如importlib.import_module())可以選擇略過__import__(),並使用它們自己的解決方案來實作引入語意。
當模組首次被引入時,Python 會搜尋該模組,若找到則會建立一個模組物件[1],並對其進行初始化。如果找不到指定的模組,則會引發ModuleNotFoundError。當引入機制被叫用時,Python 會實作各種策略來搜尋指定的模組。這些策略可以透過使用以下章節描述的各種 hook(掛鉤)來修改和擴展。
在 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__.pyone/__init__.pytwo/__init__.pythree/__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.命名空間套件¶
命名空間套件是由不同的部分 組成的,每個部分都為父套件提供一個子套件。這些部分可以位於檔案系統上的不同位置。部分可能也存在於壓縮檔案中、網路上,或 Python 在引入時搜尋的任何其他地方。命名空間套件不一定直接對應於檔案系統中的物件;它們可能是沒有具體表示的虛擬模組。
命名空間套件的__path__ 屬性不使用普通的串列。它們使用自訂的可疊代型別,當父套件的路徑(或頂層套件的sys.path)發生變化時,會在下一次引入嘗試時自動執行新一輪的套件部分搜尋。
在命名空間套件中,不存在parent/__init__.py 檔案。實際上,在引入搜尋過程中可能會找到多個parent 目錄,每個目錄由不同的部分提供。因此,parent/one 可能與parent/two 不會實際位於一起。在這種情況下,每當引入頂層parent 套件或其子套件之一時,Python 會為頂層parent 套件建立一個命名空間套件。
有關命名空間套件的規格,請參見PEP 420。
5.3.搜尋¶
在開始搜尋之前,Python 需要被引入模組(或套件,但在本討論中,兩者的區別無關緊要)的完整限定名稱 (qualified name)。此名稱可能來自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.尋檢器 (Finder) 與載入器 (Loader)¶
如果在sys.modules 中找不到指定的模組,則會叫用 Python 的引入協定來尋找並載入該模組。這個協定由兩個概念性物件組成,尋檢器 和載入器。尋檢器的任務是使用其已知的策略來確定是否能找到命名模組。實作這兩個介面的物件稱為引入器 (importer) ——當它們發現可以載入所請求的模組時,會回傳它們自己。
Python 包含多個預設的尋檢器和引入器。第一個尋檢器知道如何定位內建模組,第二個尋檢器知道如何定位凍結模組。第三個預設尋檢器會在import path 中搜尋模組。import path 是一個位置的列表,這些位置可能是檔案系統路徑或壓縮檔案,也可以擴充以搜尋任何可定位的資源,例如由 URL 識別的資源。
引入機制是可擴充的,因此可以增加新的尋檢器來擴充模組搜尋的範圍和作用域。
尋檢器實際上不會載入模組。如果它們能找到指定的模組,它們會回傳一個模組規格,這是一個模組的引入相關資訊的封裝,引入機制會在載入模組時使用這些資訊。
以下各節將更詳細地描述尋檢器和載入器的協定,包括如何建立和註冊新的尋檢器和載入器來擴充引入機制。
在 3.4 版的變更:Python 在之前的版本中,尋檢器會直接回傳載入器,而現在它們回傳的是包含載入器的模組規格。載入器仍在引入過程中使用,但其責任減少了。
5.3.3.引入掛鉤 (Import hooks)¶
引入機制的設計是可擴充的;其主要機制是引入掛鉤。引入掛鉤有兩種類型:元掛鉤 (meta hooks) 和引入路徑掛鉤。
元掛鉤會在引入處理的開始階段被呼叫,除了查找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 有三個元路徑尋檢器,一個知道如何引入內建模組,一個知道如何引入凍結模組,還有一個知道如何從import path 引入模組(即path based finder)。
在 3.4 版的變更:元路徑尋檢器的find_spec() 方法取代了find_module(),後者現在已被棄用。雖然它將繼續正常工作,但引入機制僅在尋檢器未實作find_spec() 時才會嘗試使用它。
在 3.10 版的變更:引入系統現在使用find_module() 時將引發ImportWarning。
在 3.12 版的變更:find_module() 已被移除。請改用find_spec()。
5.4.載入¶
如果找到模組規格,引入機制會在載入模組時使用該規格(以及它包含的載入器)。以下是引入過程中載入部分的大致情況:
module=Noneifspec.loaderisnotNoneandhasattr(spec.loader,'create_module'):# 這裡假設載入器上也會定義 'exec_module'module=spec.loader.create_module(spec)ifmoduleisNone:module=ModuleType(spec.name)# 與引入相關的模組屬性會在此處設定:_init_module_attrs(spec,module)ifspec.loaderisNone:# 不支援raiseImportErrorifspec.originisNoneandspec.submodule_search_locationsisnotNone:# 命名空間套件sys.modules[spec.name]=moduleelifnothasattr(spec.loader,'exec_module'):module=spec.loader.load_module(spec.name)else:sys.modules[spec.name]=moduletry:spec.loader.exec_module(module)exceptBaseException:try:delsys.modules[spec.name]exceptKeyError:passraisereturnsys.modules[spec.name]
請注意下列細節:
如果
sys.modules中已存在具有給定名稱的模組物件,引入會已回傳該物件。在載入器執行模組程式碼之前,模組將已存在於
sys.modules中。這一點至關重要,因為模組程式碼可能會(直接或間接)引入自己;事先將其增加到sys.modules可以預防類似無限遞迴以及多次重複載入等情形。如果載入失敗,只有載入失敗的模組會從
sys.modules中刪除。任何已存在於sys.modules快取中的模組,以及任何在載入失敗前成功載入的模組,都必須保留在快取中。此情形與重新載入不同,在重新載入時,即使載入失敗的模組也會保留在sys.modules中。模組建立後、在執行之前,引入機制會設定與引入相關的模組屬性(在上面的偽程式碼範例中為 "_init_module_attrs"),具體內容在之後的段落會總結。
模組執行是載入過程中的關鍵時刻,此時模組的命名空間會被新增名稱。執行過程完全交由載入器處理,由其決定如何新增以及新增什麼。
在載入過程中建立並傳遞給 exec_module() 的模組,可能不會是引入結束時回傳的模組[2]。
在 3.4 版的變更:引入系統已接管載入器的模板 (boilerplate) 責任。之前是由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() 取代,引入機制已承擔所有載入的模板責任。
為了與現有的載入器相容,引入機制會在載入器未實作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 APIs、import 或import-from 陳述式,或內建的__import__())載入子模組時,會將子模組物件繫結到父模組的命名空間中。例如,如果套件spam 有一個子模組foo,則在引入spam.foo 之後,spam 將擁有一個名為foo 的屬性,該屬性繫結到子模組。我們假設你有以下的目錄結構:
spam/__init__.pyfoo.py
並且spam/__init__.py 中包含以下程式碼:
from.fooimportFoo
那麼執行以下程式碼會將foo 和Foo 的名稱繫結到spam 模組中:
>>>importspam>>>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__ 屬性應該是一個(可能為空的)sequence,其包含列舉套件子模組位置的字串。根據定義,如果一個模組有__path__ 屬性,那麼它就是一個package。
套件的__path__ 屬性在引入其子套件時被使用。在引入機制中,其功能與sys.path 類似,即提供在引入期間搜尋模組的位置串列。然而,__path__ 通常比sys.path 更受限制。
sys.path 適用的規則同樣也適用於套件的__path__。sys.path_hooks 會在遍歷套件的__path__ 時被參考(於後文詳述)。
套件的__init__.py 檔案可以設定或修改套件的__path__ 屬性,這通常是PEP 420 之前實作命名空間套件的方式。隨著PEP 420 的採用,命名空間套件不再需要提供僅包含__path__ 操作程式碼的__init__.py 檔案;引入機制會自動為命名空間套件正確地設定__path__。
5.4.5.模組的 reprs¶
預設情況下,所有模組都有可用的 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 版的變更:module_repr() 自 Python 3.4 起被棄用,並在 Python 3.12 中移除,且不會在解析模組的 repr 時被呼叫。
5.4.6.被快取的位元組碼的無效化¶
在 Python 從.pyc 檔案載入被快取的位元組碼之前,會檢查該快取是否與來源的.py 檔案保持同步。預設情況下,Python 透過在寫入快取檔案時儲存來源檔案的最後修改時間戳和大小來完成此操作。在 runtime,引入系統會透過將快取檔案中儲存的詮釋資料 (metadata) 與來源檔案的詮釋資料進行比對來驗證快取檔案。
Python 還支援「基於雜湊」的快取檔案,這些檔案儲存源頭檔案內容的雜湊值,而不是其詮釋資料。基於雜湊的.pyc 檔案有兩種變體:需檢查和不需要檢查的。對於需檢查的基於雜湊的.pyc 檔案, Python 會對來源檔案進行雜湊,並將結果與快取檔案中的雜湊進行比較來驗證快取檔案。如果發現需檢查的基於雜湊的快取檔案無效,Python 會重新產生並寫入新的需檢查的基於雜湊的快取檔案。對於不需要檢查的基於雜湊的.pyc 檔案,只要檔案存在,Python 就假設快取檔案是有效的。可以使用--check-hash-based-pycs 旗標覆蓋基於雜湊的.pyc 檔案的驗證行為。
在 3.7 版的變更:新增了基於雜湊的.pyc 檔案。此前,Python 只支援基於時間戳的位元組碼快取無效化。
5.5.基於路徑的尋檢器¶
如前所述,Python 附帶了幾個預設的元路徑尋檢器。其中之一稱為path based finder(PathFinder),它搜尋import path,該路徑包含一個路徑條目的串列。每個路徑條目都指定了一個用於搜尋模組的位置。
基於路徑的尋檢器本身並不知道如何引入任何東西。實際上它會遍歷各個路徑條目,並將每個路徑條目與一個知道如何處理該特定路徑類型的路徑條目尋檢器關聯起來。
預設的一組路徑條目尋檢器實作了在檔案系統中尋找模組的所有語意,包括處理特殊檔案類型,例如 Python 原始程式碼檔案(.py 檔案)、Python 位元組程式碼檔案(.pyc 檔案)以及共享函式庫(例如.so 檔案)。當標準函式庫中的zipimport 模組支援時,預設的路徑條目尋檢器也能處理從壓縮檔案中載入這些檔案類型(共享函式庫除外)。
路徑條目不必侷限於檔案系統位置。它們可以參照 URL、資料庫查詢或任何可以作為字串指定的位置。
基於路徑的尋檢器提供了額外的掛鉤和協定,讓你可以擴充和自訂可搜尋的路徑條目類型。例如,如果你希望支援將路徑條目作為網路 URLs,你可以撰寫一個實作 HTTP 語意的掛鉤,用於在網路上尋找模組。這個掛鉤(一個可呼叫物件)會回傳一個支援下述協定的path entry finder ,該尋檢器隨後用於從網路中取得模組的載入器。
提醒一句:本節與前一節都使用了尋檢器 這個術語,並透過使用術語meta path finder 和path entry finder 來區分它們。這兩種類型的尋檢器非常相似,它們支援類似的協定,並在引入過程中以類似的方式運作,但請記住,它們之間仍有些許的差異。尤其元路徑尋檢器會在引入過程開始時運作,並通過sys.meta_path 的遍歷關閉 (key off)。
相比之下,路徑條目尋檢器在某種意義上是基於路徑的尋檢器的一個實作細節。事實上,如果基於路徑的尋檢器從sys.meta_path 中移除,路徑條目尋檢器的任何語意都不會被叫用。
5.5.1.路徑條目尋檢器¶
path based finder 負責尋找並載入其位置以字串path entry 指定的 Python 模組與套件。大多數路徑條目指向檔案系統中的位置,但不必侷限於此。
作為元路徑尋檢器,path based finder 實作了先前描述的find_spec() 協定,但它另外提供了可用來自訂模組如何從import path 被找到並載入的掛鉤。
path based finder 會使用三個變數:sys.path、sys.path_hooks 和sys.path_importer_cache。套件物件上的__path__ 屬性也會被使用。這些提供了額外的方法來自訂引入機制。
sys.path 包含一個字串的 list,用來提供模組與套件的搜尋位置。它會從PYTHONPATH 環境變數,以及各種安裝與實作相關的預設值初始化。sys.path 中的條目可以指定檔案系統上的目錄、zip 檔案,或其他可能需要搜尋模組的「位置」(參閱site 模組),例如 URL 或資料庫查詢。sys.path 中只能包含字串;其他資料型別都會被忽略。
path based finder 也是一種meta path finder,因此引入機制會如前所述般透過呼叫基於路徑的尋檢器的find_spec() 方法來開始import path 的搜尋。當有提供find_spec() 的path 引數時,它會是一個要遍歷的字串路徑 list——通常是在套件內引入時使用該套件的__path__ 屬性。如果path 引數為None,則表示為頂層引入並使用sys.path。
基於路徑的尋檢器會遍歷搜尋路徑中的每個條目,並為每個條目尋找適當的path entry finder (PathEntryFinder)。由於這可能是代價高昂的操作(例如此搜尋可能會有stat() 呼叫的額外開銷),基於路徑的尋檢器會維護一個將路徑條目對映到路徑條目尋檢器的快取。此快取存放於sys.path_importer_cache(儘管名稱如此,該快取實際上儲存的是尋檢器物件,而非僅限於importer 物件)。如此一來,針對特定path entry 位置的path entry finder 的高代價搜尋只需進行一次。使用者程式碼可以移除sys.path_importer_cache 中的快取條目,以強制基於路徑的尋檢器再次執行路徑條目搜尋。
如果該路徑條目不在快取中,基於路徑的尋檢器會遍歷sys.path_hooks 中的每個可呼叫物件。此 list 中的每個路徑條目掛鉤 都會以單一引數被呼叫,即要搜尋的路徑條目。這個可呼叫物件可以回傳一個能處理該路徑條目的path entry finder,也可以引發ImportError。基於路徑的尋檢器會使用ImportError 來表示該掛鉤無法為該path entry 找到path entry finder。此例外會被忽略,並繼續疊代import path。該掛鉤應預期接收字串或 bytes 物件;bytes 物件的編碼由掛鉤決定(例如檔案系統編碼、UTF-8 或其他),若掛鉤無法解碼該引數,則應引發ImportError。
若sys.path_hooks 的疊代結束後仍未回傳任何path entry finder,則基於路徑的尋檢器的find_spec() 方法會在sys.path_importer_cache 中存入None(表示此路徑條目沒有尋檢器),並回傳None,表示此meta path finder 無法找到該模組。
若sys.path_hooks 上的某個路徑條目掛鉤 可呼叫物件確實 回傳了path entry finder,則會使用以下協定向該尋檢器要求模組規格,並在載入模組時使用該規格。
目前工作目錄——以空字串表示——的處理方式與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"(只有一個例外)。
為了向引入機制表明該規格代表一個命名空間portion,路徑條目尋檢器會將submodule_search_locations 設為包含該部分的 list。
在 3.4 版的變更:find_spec() 已取代find_loader() 與find_module(),兩者現已棄用,但若未定義find_spec() 仍會被使用。
較舊的路徑條目尋檢器可能會實作這兩個已棄用的方法之一,而不是find_spec()。為了向後相容,這些方法仍會被使用。然而,如果路徑條目尋檢器實作了find_spec(),這些舊方法就會被忽略。
find_loader() 接收一個引數,即正在引入的模組之完整限定名稱。find_loader() 會回傳一個 2-tuple,其中第一個項目是 loader,第二個項目是命名空間portion。
為了與其他引入協定的實作向後相容,許多路徑條目尋檢器也支援元路徑尋檢器所支援的相同、傳統的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 的預設內容,並以自訂的元路徑掛鉤完全取代它們。
如果可以只改變 import 陳述式的行為,而不影響其他存取引入系統的 API,那麼替換內建的__import__() 函式可能就足夠了。
若要從元路徑較早處的掛鉤選擇性地阻止某些模組被引入(而不是完全停用標準引入系統),只要在find_spec() 直接引發ModuleNotFoundError,而不是回傳None 即可。後者表示元路徑搜尋應繼續,而引發例外則會立即終止。
5.7.套件相對引入¶
相對引入使用前導點號。一個前導點號表示從目前套件開始的相對引入。兩個或更多前導點號表示相對引入到目前套件的父層,第一個之後每多一個點號就往上一層。例如,給定以下套件配置:
package/__init__.pysubpackage1/__init__.pymoduleX.pymoduleY.pysubpackage2/__init__.pymoduleZ.pymoduleA.py
在subpackage1/moduleX.py 或subpackage1/__init__.py 中,以下皆為有效的相對引入:
from.moduleYimportspamfrom.moduleYimportspamashamfrom.importmoduleYfrom..subpackage1importmoduleYfrom..subpackage2.moduleZimporteggsfrom..moduleAimportfoo
絕對引入可以使用import<> 或from<>import<> 語法,但相對引入只能使用第二種形式;原因是:
importXXX.YYY.ZZZ
應該要將XXX.YYY.ZZZ 作為可用的運算式公開,但 .moduleY 不是有效的運算式。
5.8.__main__ 的特殊考量¶
__main__ 模組相對於 Python 的引入系統而言是一個特殊案例。如elsewhere 所述,__main__ 模組會在直譯器啟動時直接初始化,類似於sys 與builtins。然而,與那兩者不同,它並不嚴格算是內建模組。這是因為__main__ 的初始化方式取決於呼叫直譯器時使用的旗標與其他選項。
5.8.1.__main__.__spec__¶
視__main__ 的初始化方式而定,__main__.__spec__ 會被適當設定,或設為None。
當 Python 以-m 選項啟動時,__spec__ 會設定為對應模組或套件的模組規格。當__main__ 模組作為執行目錄、zipfile 或其他sys.path 條目的一部分而被載入時,__spec__ 也會被填入。
在其餘狀況 中,__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 得以棄用,並新增尋檢器與載入器的方法。
註解
[1][2]importlib 的實作避免直接使用回傳值,而是透過在sys.modules 中查找模組名稱來取得模組物件。這樣的間接效果是,被引入的模組可能會在sys.modules 中替換自己。這是實作特定的行為,並不保證能在其他 Python 實作中運作。