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__.py
    one/
        __init__.py
    two/
        __init__.py
    three/
        __init__.py

引入 parent.one 將隱式執行 parent/__init__.pyparent/one/__init__.py。隨後引入 parent.twoparent.three 將分別執行 parent/two/__init__.pyparent/three/__init__.py

A subdirectory inside a regular package that does not contain an __init__.py file is treated as an implicit namespace package (a "namespace subpackage") rooted in that parent. See PEP 420 for the underlying specification.

5.2.2. 命名空間套件

命名空間套件是由不同的部分 組成的,每個部分都為父套件提供一個子套件。這些部分可以位於檔案系統上的不同位置。部分可能也存在於壓縮檔案中、網路上,或 Python 在引入時搜尋的任何其他地方。命名空間套件不一定直接對應於檔案系統中的物件;它們可能是沒有具體表示的虛擬模組。

命名空間套件的 __path__ 屬性不使用普通的串列。它們使用自訂的可疊代型別,當父套件的路徑(或頂層套件的 sys.path)發生變化時,會在下一次引入嘗試時自動執行新一輪的套件部分搜尋。

在命名空間套件中,不存在 parent/__init__.py 檔案。實際上,在引入搜尋過程中可能會找到多個 parent 目錄,每個目錄由不同的部分提供。因此,parent/one 可能與 parent/two 不會實際位於一起。在這種情況下,每當引入頂層 parent 套件或其子套件之一時,Python 會為頂層 parent 套件建立一個命名空間套件。

Namespace packages may also be nested inside a regular package. When the import system searches a regular package's __path__ and encounters a subdirectory that does not contain an __init__.py file, that subdirectory becomes a portion contributing to a namespace subpackage of the enclosing regular package.

有關命名空間套件的規格,請參見 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 將包含 foofoo.barfoo.bar.baz 的條目。每個鍵的值都是相應的模組物件。

在引入過程中,會在 sys.modules 中查找模組名稱,如果存在,則相關的值為滿足此引入的模組,此引入過程即完成。然而,如果值是 None,則會引發 ModuleNotFoundError。如果模組名稱不存在,Python 會繼續搜尋該模組。

sys.modules 是可寫入的。刪除一個鍵可能不會銷毀相關聯的模組(因為其他模組可能持有對它的參照),但會使指定的模組的快取條目失效,導致 Python 在下一次引入該模組時重新搜尋。也可以將鍵賦值為 None,這會強制下一次引入該模組時引發 ModuleNotFoundError

但請注意,如果你保留了對模組物件的參照,並在 sys.modules 中使其快取條目失效,然後重新引入指定的模組,這兩個模組物件將不會相同。相比之下,importlib.reload() 會重用相同的模組物件,並透過重新執行模組的程式碼來簡單地重新初始化模組內容。

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.5. 基於路徑的尋檢器

如前所述,Python 附帶了幾個預設的元路徑尋檢器。其中之一稱為 path based finderPathFinder),它搜尋 import path,該路徑包含一個路徑條目的串列。每個路徑條目都指定了一個用於搜尋模組的位置。

基於路徑的尋檢器本身並不知道如何引入任何東西。實際上它會遍歷各個路徑條目,並將每個路徑條目與一個知道如何處理該特定路徑類型的路徑條目尋檢器關聯起來。

預設的一組路徑條目尋檢器實作了在檔案系統中尋找模組的所有語意,包括處理特殊檔案類型,例如 Python 原始程式碼檔案(.py 檔案)、Python 位元組程式碼檔案(.pyc 檔案)以及共享函式庫(例如 .so 檔案)。當標準函式庫中的 zipimport 模組支援時,預設的路徑條目尋檢器也能處理從壓縮檔案中載入這些檔案類型(共享函式庫除外)。

路徑條目不必侷限於檔案系統位置。它們可以參照 URL、資料庫查詢或任何可以作為字串指定的位置。

基於路徑的尋檢器提供了額外的掛鉤和協定,讓你可以擴充和自訂可搜尋的路徑條目類型。例如,如果你希望支援將路徑條目作為網路 URLs,你可以撰寫一個實作 HTTP 語意的掛鉤,用於在網路上尋找模組。這個掛鉤(一個可呼叫物件)會回傳一個支援下述協定的 path entry finder ,該尋檢器隨後用於從網路中取得模組的載入器。

提醒一句:本節與前一節都使用了 尋檢器 這個術語,並透過使用術語 meta path finderpath 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.pathsys.path_hookssys.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__.py
    subpackage1/
        __init__.py
        moduleX.py
        moduleY.py
    subpackage2/
        __init__.py
        moduleZ.py
    moduleA.py

subpackage1/moduleX.pysubpackage1/__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 的引入系統而言是一個特殊案例。如 elsewhere 所述,__main__ 模組會在直譯器啟動時直接初始化,類似於 sysbuiltins。然而,與那兩者不同,它並不嚴格算是內建模組。這是因為 __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 得以棄用,並新增尋檢器與載入器的方法。

註解