5. 導入系統(tǒng)?

一個 module 內(nèi)的 Python 代碼通過 importing 操作就能夠訪問另一個模塊內(nèi)的代碼。 import 語句是發(fā)起調(diào)用導入機制的最常用方式,但不是唯一的方式。 importlib.import_module() 以及內(nèi)置的 __import__() 等函數(shù)也可以被用來發(fā)起調(diào)用導入機制。

import 語句結合了兩個操作;它先搜索指定名稱的模塊,然后將搜索結果綁定到當前作用域中的名稱。 import 語句的搜索操作被定義為對 __import__() 函數(shù)的調(diào)用并帶有適當?shù)膮?shù)。 __import__() 的返回值會被用于執(zhí)行 import 語句的名稱綁定操作。 請參閱 import 語句了解名稱綁定操作的更多細節(jié)。

__import__() 的直接調(diào)用將僅執(zhí)行模塊搜索以及在找到時的模塊創(chuàng)建操作。 不過也可能產(chǎn)生某些副作用,例如導入父包和更新各種緩存 (包括 sys.modules),只有 import 語句會執(zhí)行名稱綁定操作。

import 語句被執(zhí)行時,標準的內(nèi)置 __import__() 函數(shù)會被調(diào)用。 其他發(fā)起調(diào)用導入系統(tǒng)的機制 (例如 importlib.import_module()) 可能會選擇繞過 __import__() 并使用它們自己的解決方案來實現(xiàn)導入機制。

當一個模塊首次被導入時,Python 會搜索該模塊,如果找到就創(chuàng)建一個 module 對象 1 并初始化它。 如果指定名稱的模塊未找到,則會引發(fā) ModuleNotFoundError。 當發(fā)起調(diào)用導入機制時,Python 會實現(xiàn)多種策略來搜索指定名稱的模塊。 這些策略可以通過使用使用下文所描述的多種鉤子來加以修改和擴展。

在 3.3 版更改: 導入系統(tǒng)已被更新以完全實現(xiàn) PEP 302 中的第二階段要求。 不會再有任何隱式的導入機制 —— 整個導入系統(tǒng)都通過 sys.meta_path 暴露出來。 此外,對原生命名空間包的支持也已被實現(xiàn) (參見 PEP 420)。

5.1. importlib?

importlib 模塊提供了一個豐富的 API 用來與導入系統(tǒng)進行交互。 例如 importlib.import_module() 提供了相比內(nèi)置的 __import__() 更推薦、更簡單的 API 用來發(fā)起調(diào)用導入機制。 更多細節(jié)請參看 importlib 庫文檔。

5.2. ?

Python 只有一種模塊對象類型,所有模塊都屬于該類型,無論模塊是用 Python、C 還是別的語言實現(xiàn)。 為了幫助組織模塊并提供名稱層次結構,Python 還引入了 的概念。

你可以把包看成是文件系統(tǒng)中的目錄,并把模塊看成是目錄中的文件,但請不要對這個類比做過于字面的理解,因為包和模塊不是必須來自于文件系統(tǒng)。 為了方便理解本文檔,我們將繼續(xù)使用這種目錄和文件的類比。 與文件系統(tǒng)一樣,包通過層次結構進行組織,在包之內(nèi)除了一般的模塊,還可以有子包。

要注意的一個重點概念是所有包都是模塊,但并非所有模塊都是包。 或者換句話說,包只是一種特殊的模塊。 特別地,任何具有 __path__ 屬性的模塊都會被當作是包。

All modules have a name. Subpackage names are separated from their parent package name by a dot, akin to Python's standard attribute access syntax. Thus you might have a package called email, which in turn has a subpackage called email.mime and a module within that subpackage called email.mime.text.

5.2.1. 常規(guī)包?

Python 定義了兩種類型的包,常規(guī)包命名空間包。 常規(guī)包是傳統(tǒng)的包類型,它們在 Python 3.2 及之前就已存在。 常規(guī)包通常以一個包含 __init__.py 文件的目錄形式實現(xiàn)。 當一個常規(guī)包被導入時,這個 __init__.py 文件會隱式地被執(zhí)行,它所定義的對象會被綁定到該包命名空間中的名稱。__init__.py 文件可以包含與任何其他模塊中所包含的 Python 代碼相似的代碼,Python 將在模塊被導入時為其添加額外的屬性。

例如,以下文件系統(tǒng)布局定義了一個最高層級的 parent 包和三個子包:

parent/
    __init__.py
    one/
        __init__.py
    two/
        __init__.py
    three/
        __init__.py

導入 parent.one 將隱式地執(zhí)行 parent/__init__.pyparent/one/__init__.py。 后續(xù)導入 parent.twoparent.three 則將分別執(zhí)行 parent/two/__init__.pyparent/three/__init__.py

5.2.2. 命名空間包?

命名空間包是由多個 部分 構成的,每個部分為父包增加一個子包。 各個部分可能處于文件系統(tǒng)的不同位置。 部分也可能處于 zip 文件中、網(wǎng)絡上,或者 Python 在導入期間可以搜索的其他地方。 命名空間包并不一定會直接對應到文件系統(tǒng)中的對象;它們有可能是無實體表示的虛擬模塊。

命名空間包的 __path__ 屬性不使用普通的列表。 而是使用定制的可迭代類型,如果其父包的路徑 (或者最高層級包的 sys.path) 發(fā)生改變,這種對象會在該包內(nèi)的下一次導入嘗試時自動執(zhí)行新的對包部分的搜索。

命名空間包沒有 parent/__init__.py 文件。 實際上,在導入搜索期間可能找到多個 parent 目錄,每個都由不同的部分所提供。 因此 parent/one 的物理位置不一定與 parent/two 相鄰。 在這種情況下,Python 將為頂級的 parent 包創(chuàng)建一個命名空間包,無論是它本身還是它的某個子包被導入。

另請參閱 PEP 420 了解對命名空間包的規(guī)格描述。

5.3. 搜索?

為了開始搜索,Python 需要被導入模塊(或者包,對于當前討論來說兩者沒有差別)的完整 限定名稱。 此名稱可以來自 import 語句所帶的各種參數(shù),或者來自傳給 importlib.import_module()__import__() 函數(shù)的形參。

此名稱會在導入搜索的各個階段被使用,它也可以是指向一個子模塊的帶點號路徑,例如 foo.bar.baz。 在這種情況下,Python 會先嘗試導入 foo,然后是 foo.bar,最后是 foo.bar.baz。 如果這些導入中的任何一個失敗,都會引發(fā) ModuleNotFoundError。

5.3.1. 模塊緩存?

在導入搜索期間首先會被檢查的地方是 sys.modules。 這個映射起到緩存之前導入的所有模塊的作用(包括其中間路徑)。 因此如果之前導入過 foo.bar.baz,則 sys.modules 將包含 foo, foo.barfoo.bar.baz 條目。 每個鍵的值就是相應的模塊對象。

在導入期間,會在 sys.modules 查找模塊名稱,如存在則其關聯(lián)的值就是需要導入的模塊,導入過程完成。 然而,如果值為 None,則會引發(fā) ModuleNotFoundError。 如果找不到指定模塊名稱,Python 將繼續(xù)搜索該模塊。

sys.modules 是可寫的。刪除鍵可能不會破壞關聯(lián)的模塊(因為其他模塊可能會保留對它的引用),但它會使命名模塊的緩存條目無效,導致 Python 在下次導入時重新搜索命名模塊。鍵也可以賦值為 None ,強制下一次導入模塊導致 ModuleNotFoundError 。

但是要小心,因為如果你還保有對某個模塊對象的引用,同時停用其在 sys.modules 中的緩存條目,然后又再次導入該名稱的模塊,則前后兩個模塊對象將 不是 同一個。 相反地,importlib.reload() 將重用 同一個 模塊對象,并簡單地通過重新運行模塊的代碼來重新初始化模塊內(nèi)容。

5.3.2. 查找器和加載器?

如果指定名稱的模塊在 sys.modules 找不到,則將發(fā)起調(diào)用 Python 的導入?yún)f(xié)議以查找和加載該模塊。 此協(xié)議由兩個概念性模塊構成,即 查找器加載器。 查找器的任務是確定是否能使用其所知的策略找到該名稱的模塊。 同時實現(xiàn)這兩種接口的對象稱為 導入器 —— 它們在確定能加載所需的模塊時會返回其自身。

Python 包含了多個默認查找器和導入器。 第一個知道如何定位內(nèi)置模塊,第二個知道如何定位凍結模塊。 第三個默認查找器會在 import path 中搜索模塊。 import path 是一個由文件系統(tǒng)路徑或 zip 文件組成的位置列表。 它還可以擴展為搜索任意可定位資源,例如由 URL 指定的資源。

導入機制是可擴展的,因此可以加入新的查找器以擴展模塊搜索的范圍和作用域。

查找器并不真正加載模塊。 如果它們能找到指定名稱的模塊,會返回一個 模塊規(guī)格說明,這是對模塊導入相關信息的封裝,供后續(xù)導入機制用于在加載模塊時使用。

以下各節(jié)描述了有關查找器和加載器協(xié)議的更多細節(jié),包括你應該如何創(chuàng)建并注冊新的此類對象來擴展導入機制。

在 3.4 版更改: 在之前的 Python 版本中,查找器會直接返回 加載器,現(xiàn)在它們則返回模塊規(guī)格說明,其中 包含 加載器。 加載器仍然在導入期間被使用,但負擔的任務有所減少。

5.3.3. 導入鉤子?

導入機制被設計為可擴展;其中的基本機制是 導入鉤子。 導入鉤子有兩種類型: 元鉤子導入路徑鉤子。

元鉤子在導入過程開始時被調(diào)用,此時任何其他導入過程尚未發(fā)生,但 sys.modules 緩存查找除外。 這允許元鉤子重載 sys.path 過程、凍結模塊甚至內(nèi)置模塊。 元鉤子的注冊是通過向 sys.meta_path 添加新的查找器對象,具體如下所述。

導入路徑鉤子是作為 sys.path (或 package.__path__) 過程的一部分,在遇到它們所關聯(lián)的路徑項的時候被調(diào)用。 導入路徑鉤子的注冊是通過向 sys.path_hooks 添加新的可調(diào)用對象,具體如下所述。

5.3.4. 元路徑?

當指定名稱的模塊在 sys.modules 中找不到時,Python 會接著搜索 sys.meta_path,其中包含元路徑查找器對象列表。 這些查找器按順序被查詢以確定它們是否知道如何處理該名稱的模塊。 元路徑查找器必須實現(xiàn)名為 find_spec() 的方法,該方法接受三個參數(shù):名稱、導入路徑和目標模塊(可選)。 元路徑查找器可使用任何策略來確定它是否能處理指定名稱的模塊。

如果元路徑查找器知道如何處理指定名稱的模塊,它將返回一個說明對象。 如果它不能處理該名稱的模塊,則會返回 None。 如果 sys.meta_path 處理過程到達列表末尾仍未返回說明對象,則將引發(fā) ModuleNotFoundError。 任何其他被引發(fā)異常將直接向上傳播,并放棄導入過程。

元路徑查找器的 find_spec() 方法調(diào)用帶有兩到三個參數(shù)。 第一個是被導入模塊的完整限定名稱,例如 foo.bar.baz。 第二個參數(shù)是供模塊搜索使用的路徑條目。 對于最高層級模塊,第二個參數(shù)為 None,但對于子模塊或子包,第二個參數(shù)為父包 __path__ 屬性的值。 如果相應的 __path__ 屬性無法訪問,將引發(fā) ModuleNotFoundError。 第三個參數(shù)是一個將被作為稍后加載目標的現(xiàn)有模塊對象。 導入系統(tǒng)僅會在重加載期間傳入一個目標模塊。

對于單個導入請求可以多次遍歷元路徑。 例如,假設所涉及的模塊都尚未被緩存,則導入 foo.bar.baz 將首先執(zhí)行頂級的導入,在每個元路徑查找器 (mpf) 上調(diào)用 mpf.find_spec("foo", None, None)。 在導入 foo 之后,foo.bar 將通過第二次遍歷元路徑來導入,調(diào)用 mpf.find_spec("foo.bar", foo.__path__, None)。 一旦 foo.bar 完成導入,最后一次遍歷將調(diào)用 mpf.find_spec("foo.bar.baz", foo.bar.__path__, None)。

有些元路徑查找器只支持頂級導入。 當把 None 以外的對象作為第三個參數(shù)傳入時,這些導入器將總是返回 None

Python 的默認 sys.meta_path 具有三種元路徑查找器,一種知道如何導入內(nèi)置模塊,一種知道如何導入凍結模塊,還有一種知道如何導入來自 import path 的模塊 (即 path based finder)。

在 3.4 版更改: 元路徑查找器的 find_spec() 方法替代了 find_module(),后者現(xiàn)已棄用,它將繼續(xù)可用但不會再做改變,導入機制僅會在查找器未實現(xiàn) find_spec() 時嘗試使用它。

在 3.10 版更改: 導入系統(tǒng)使用 find_module() 現(xiàn)在將會引發(fā) ImportWarning。

5.4. 加載?

當一個模塊說明被找到時,導入機制將在加載該模塊時使用它(及其所包含的加載器)。 下面是導入的加載部分所發(fā)生過程的簡要說明:

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)
    # Set __loader__ and __package__ if missing.
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]

請注意以下細節(jié):

  • 如果在 sys.modules 中存在指定名稱的模塊對象,導入操作會已經(jīng)將其返回。

  • 在加載器執(zhí)行模塊代碼之前,該模塊將存在于 sys.modules 中。 這一點很關鍵,因為該模塊代碼可能(直接或間接地)導入其自身;預先將其添加到 sys.modules 可防止在最壞情況下的無限遞歸和最好情況下的多次加載。

  • 如果加載失敗,則該模塊 -- 只限加載失敗的模塊 -- 將從 sys.modules 中移除。 任何已存在于 sys.modules 緩存的模塊,以及任何作為附帶影響被成功加載的模塊仍會保留在緩存中。 這與重新加載不同,后者會把即使加載失敗的模塊也保留在 sys.modules 中。

  • 在模塊創(chuàng)建完成但還未執(zhí)行之前,導入機制會設置導入相關模塊屬性(在上面的示例偽代碼中為 “_init_module_attrs”),詳情參見 后續(xù)部分。

  • 模塊執(zhí)行是加載的關鍵時刻,在此期間將填充模塊的命名空間。 執(zhí)行會完全委托給加載器,由加載器決定要填充的內(nèi)容和方式。

  • 在加載過程中創(chuàng)建并傳遞給 exec_module() 的模塊并不一定就是在導入結束時返回的模塊 2。

在 3.4 版更改: 導入系統(tǒng)已經(jīng)接管了加載器建立樣板的責任。 這些在以前是由 importlib.abc.Loader.load_module() 方法來執(zhí)行的。

5.4.1. 加載器?

模塊加載器提供關鍵的加載功能:模塊執(zhí)行。 導入機制調(diào)用 importlib.abc.Loader.exec_module() 方法并傳入一個參數(shù)來執(zhí)行模塊對象。 從 exec_module() 返回的任何值都將被忽略。

加載器必須滿足下列要求:

  • 如果模塊是一個 Python 模塊(而非內(nèi)置模塊或動態(tài)加載的擴展),加載器應該在模塊的全局命名空間 (module.__dict__) 中執(zhí)行模塊的代碼。

  • 如果加載器無法執(zhí)行指定模塊,它應該引發(fā) ImportError,不過在 exec_module() 期間引發(fā)的任何其他異常也會被傳播。

在許多情況下,查找器和加載器可以是同一對象;在此情況下 find_spec() 方法將返回一個規(guī)格說明,其中加載器會被設為 self

模塊加載器可以選擇通過實現(xiàn) create_module() 方法在加載期間創(chuàng)建模塊對象。 它接受一個參數(shù),即模塊規(guī)格說明,并返回新的模塊對象供加載期間使用。 create_module() 不需要在模塊對象上設置任何屬性。 如果模塊返回 None,導入機制將自行創(chuàng)建新模塊。

3.4 新版功能: 加載器的 create_module() 方法。

在 3.4 版更改: load_module() 方法被 exec_module() 所替代,導入機制會對加載的所有樣板責任作出假定。

為了與現(xiàn)有的加載器兼容,導入機制會使用導入器的 load_module() 方法,如果它存在且導入器也未實現(xiàn) exec_module()。 但是,load_module() 現(xiàn)已棄用,加載器應該轉而實現(xiàn) exec_module()

除了執(zhí)行模塊之外,load_module() 方法必須實現(xiàn)上文描述的所有樣板加載功能。 所有相同的限制仍然適用,并帶有一些附加規(guī)定:

  • 如果 sys.modules 中存在指定名稱的模塊對象,加載器必須使用已存在的模塊。 (否則 importlib.reload() 將無法正確工作。) 如果該名稱模塊不存在于 sys.modules 中,加載器必須創(chuàng)建一個新的模塊對象并將其加入 sys.modules

  • 在加載器執(zhí)行模塊代碼之前,模塊 必須 存在于 sys.modules 之中,以防止無限遞歸或多次加載。

  • 如果加載失敗,加載器必須移除任何它已加入到 sys.modules 中的模塊,但它必須 僅限 移除加載失敗的模塊,且所移除的模塊應為加載器自身顯式加載的。

在 3.5 版更改: exec_module() 已定義但 create_module() 未定義時將引發(fā) DeprecationWarning。

在 3.6 版更改: exec_module() 已定義但 create_module() 未定義時將引發(fā) ImportError。

在 3.10 版更改: 使用 load_module() 將引發(fā) ImportWarning。

5.4.2. 子模塊?

當使用任意機制 (例如 importlib API, importimport-from 語句或者內(nèi)置的 __import__()) 加載一個子模塊時,父模塊的命名空間中會添加一個對子模塊對象的綁定。 例如,如果包 spam 有一個子模塊 foo,則在導入 spam.foo 之后,spam 將具有一個 綁定到相應子模塊的 foo 屬性。 假如現(xiàn)在有如下的目錄結構:

spam/
    __init__.py
    foo.py

and spam/__init__.py has the following line in it:

from .foo import Foo

then executing the following puts name bindings for foo and Foo in the spam module:

>>>
>>> import spam
>>> spam.foo
<module 'spam.foo' from '/tmp/imports/spam/foo.py'>
>>> spam.Foo
<class 'spam.foo.Foo'>

按照通常的 Python 名稱綁定規(guī)則,這看起來可能會令人驚訝,但它實際上是導入系統(tǒng)的一個基本特性。 保持不變的一點是如果你有 sys.modules['spam']sys.modules['spam.foo'] (例如在上述導入之后就是如此),則后者必須顯示為前者的 foo 屬性。

5.4.3. 模塊規(guī)格說明?

導入機制在導入期間會使用有關每個模塊的多種信息,特別是加載之前。 大多數(shù)信息都是所有模塊通用的。 模塊規(guī)格說明的目的是基于每個模塊來封裝這些導入相關信息。

在導入期間使用規(guī)格說明可允許狀態(tài)在導入系統(tǒng)各組件之間傳遞,例如在創(chuàng)建模塊規(guī)格說明的查找器和執(zhí)行模塊的加載器之間。 最重要的一點是,它允許導入機制執(zhí)行加載的樣板操作,在沒有模塊規(guī)格說明的情況下這是加載器的責任。

模塊的規(guī)格說明會作為模塊對象的 __spec__ 屬性對外公開。 有關模塊規(guī)格的詳細內(nèi)容請參閱 ModuleSpec

3.4 新版功能.

5.4.5. module.__path__?

根據(jù)定義,如果一個模塊具有 __path__ 屬性,它就是包。

包的 __path__ 屬性會在導入其子包期間被使用。 在導入機制內(nèi)部,它的功能與 sys.path 基本相同,即在導入期間提供一個模塊搜索位置列表。 但是,__path__ 通常會比 sys.path 受到更多限制。

__path__ 必須是由字符串組成的可迭代對象,但它也可以為空。 作用于 sys.path 的規(guī)則同樣適用于包的 __path__,并且 sys.path_hooks (見下文) 會在遍歷包的 __path__ 時被查詢。

包的 __init__.py 文件可以設置或更改包的 __path__ 屬性,而且這是在 PEP 420 之前實現(xiàn)命名空間包的典型方式。 隨著 PEP 420 的引入,命名空間包不再需要提供僅包含 __path__ 操控代碼的 __init__.py 文件;導入機制會自動為命名空間包正確地設置 __path__。

5.4.6. 模塊的 repr?

默認情況下,全部模塊都具有一個可用的 repr,但是你可以依據(jù)上述的屬性設置,在模塊的規(guī)格說明中更為顯式地控制模塊對象的 repr。

如果模塊具有 spec (__spec__),導入機制將嘗試用它來生成一個 repr。 如果生成失敗或找不到 spec,導入系統(tǒng)將使用模塊中的各種可用信息來制作一個默認 repr。 它將嘗試使用 module.__name__, module.__file__ 以及 module.__loader__ 作為 repr 的輸入,并將任何丟失的信息賦為默認值。

以下是所使用的確切規(guī)則:

  • 如果模塊具有 __spec__ 屬性,其中的規(guī)格信息會被用來生成 repr。 被查詢的屬性有 "name", "loader", "origin" 和 "has_location" 等等。

  • 如果模塊具有 __file__ 屬性,這會被用作模塊 repr 的一部分。

  • 如果模塊沒有 __file__ 但是有 __loader__ 且取值不為 None,則加載器的 repr 會被用作模塊 repr 的一部分。

  • 對于其他情況,僅在 repr 中使用模塊的 __name__。

在 3.4 版更改: loader.module_repr() 已棄用,導入機制現(xiàn)在使用模塊規(guī)格說明來生成模塊 repr。

為了向后兼容 Python 3.3,如果加載器定義了 module_repr() 方法,則會在嘗試上述兩種方式之前先調(diào)用該方法來生成模塊 repr。 但請注意此方法已棄用。

在 3.10 版更改: module_repr() 的調(diào)用現(xiàn)在會在嘗試使用模塊的 __spec__ 屬性之后但在回退至 __file__ 之前發(fā)生。 module_repr() 的使用預定會在 Python 3.12 中停止。

5.4.7. 已緩存字節(jié)碼的失效?

在 Python 從 .pyc 文件加載已緩存字節(jié)碼之前,它會檢查緩存是否由最新的 .py 源文件所生成。 默認情況下,Python 通過在所寫入緩存文件中保存源文件的最新修改時間戳和大小來實現(xiàn)這一點。 在運行時,導入系統(tǒng)會通過比對緩存文件中保存的元數(shù)據(jù)和源文件的元數(shù)據(jù)確定該緩存的有效性。

Python 也支持“基于哈希的”緩存文件,即保存源文件內(nèi)容的哈希值而不是其元數(shù)據(jù)。 存在兩種基于哈希的 .pyc 文件:檢查型和非檢查型。 對于檢查型基于哈希的 .pyc 文件,Python 會通過求哈希源文件并將結果哈希值與緩存文件中的哈希值比對來確定緩存有效性。 如果檢查型基于哈希的緩存文件被確定為失效,Python 會重新生成并寫入一個新的檢查型基于哈希的緩存文件。 對于非檢查型 .pyc 文件,只要其存在 Python 就會直接認定緩存文件有效。 確定基于哈希的 .pyc 文件有效性的行為可通過 --check-hash-based-pycs 旗標來重載。

在 3.7 版更改: 增加了基于哈希的 .pyc 文件。在此之前,Python 只支持基于時間戳來確定字節(jié)碼緩存的有效性。

5.5. 基于路徑的查找器?

在之前已經(jīng)提及,Python 帶有幾種默認的元路徑查找器。 其中之一是 path based finder (PathFinder),它會搜索包含一個 路徑條目 列表的 import path。 每個路徑條目指定一個用于搜索模塊的位置。

基于路徑的查找器自身并不知道如何進行導入。 它只是遍歷單獨的路徑條目,將它們各自關聯(lián)到某個知道如何處理特定類型路徑的路徑條目查找器。

默認的路徑條目查找器集合實現(xiàn)了在文件系統(tǒng)中查找模塊的所有語義,可處理多種特殊文件類型例如 Python 源碼 (.py 文件),Python 字節(jié)碼 (.pyc 文件) 以及共享庫 (例如 .so 文件)。 在標準庫中 zipimport 模塊的支持下,默認路徑條目查找器還能處理所有來自 zip 文件的上述文件類型。

路徑條目不必僅限于文件系統(tǒng)位置。 它們可以指向 URL、數(shù)據(jù)庫查詢或可以用字符串指定的任何其他位置。

基于路徑的查找器還提供了額外的鉤子和協(xié)議以便能擴展和定制可搜索路徑條目的類型。 例如,如果你想要支持網(wǎng)絡 URL 形式的路徑條目,你可以編寫一個實現(xiàn) HTTP 語義在網(wǎng)絡上查找模塊的鉤子。 這個鉤子(可調(diào)用對象)應當返回一個支持下述協(xié)議的 path entry finder,以被用來獲取一個專門針對來自網(wǎng)絡的模塊的加載器。

預先的警告:本節(jié)和上節(jié)都使用了 查找器 這一術語,并通過 meta path finderpath entry finder 兩個術語來明確區(qū)分它們。 這兩種類型的查找器非常相似,支持相似的協(xié)議,且在導入過程中以相似的方式運作,但關鍵的一點是要記住它們是有微妙差異的。 特別地,元路徑查找器作用于導入過程的開始,主要是啟動 sys.meta_path 遍歷。

相比之下,路徑條目查找器在某種意義上說是基于路徑的查找器的實現(xiàn)細節(jié),實際上,如果需要從 sys.meta_path 移除基于路徑的查找器,并不會有任何路徑條目查找器被發(fā)起調(diào)用。

5.5.1. 路徑條目查找器?

path based finder 會負責查找和加載通過 path entry 字符串來指定位置的 Python 模塊和包。 多數(shù)路徑條目所指定的是文件系統(tǒng)中的位置,但它們并不必受限于此。

作為一種元路徑查找器,path based finder 實現(xiàn)了上文描述的 find_spec() 協(xié)議,但是它還對外公開了一些附加鉤子,可被用來定制模塊如何從 import path 查找和加載。

有三個變量由 path based finder, sys.path, sys.path_hookssys.path_importer_cache 所使用。 包對象的 __path__ 屬性也會被使用。 它們提供了可用于定制導入機制的額外方式。

sys.path 包含一個提供模塊和包搜索位置的字符串列表。 它初始化自 PYTHONPATH 環(huán)境變量以及多種其他特定安裝和實現(xiàn)的默認設置。 sys.path 條目可指定的名稱有文件系統(tǒng)中的目錄、zip 文件和其他可用于搜索模塊的潛在“位置”(參見 site 模塊),例如 URL 或數(shù)據(jù)庫查詢等。 在 sys.path 中只能出現(xiàn)字符串和字節(jié)串;所有其他數(shù)據(jù)類型都會被忽略。 字節(jié)串條目使用的編碼由單獨的 路徑條目查找器 來確定。

path based finder 是一種 meta path finder,因此導入機制會通過調(diào)用上文描述的基于路徑的查找器的 find_spec() 方法來啟動 import path 搜索。 當要向 find_spec() 傳入 path 參數(shù)時,它將是一個可遍歷的字符串列表 —— 通常為用來在其內(nèi)部進行導入的包的 __path__ 屬性。 如果 path 參數(shù)為 None,這表示最高層級的導入,將會使用 sys.path。

基于路徑的查找器會迭代搜索路徑中的每個條目,并且每次都查找與路徑條目對應的 path entry finder (PathEntryFinder)。 因為這種操作可能很耗費資源(例如搜索會有 stat() 調(diào)用的開銷),基于路徑的查找器會維持一個緩存來將路徑條目映射到路徑條目查找器。 這個緩存放于 sys.path_importer_cache (盡管如此命名,但這個緩存實際存放的是查找器對象而非僅限于 importer 對象)。 通過這種方式,對特定 path entry 位置的 path entry finder 的高耗費搜索只需進行一次。 用戶代碼可以自由地從 sys.path_importer_cache 移除緩存條目,以強制基于路徑的查找器再次執(zhí)行路徑條目搜索 3。

如果路徑條目不存在于緩存中,基于路徑的查找器會迭代 sys.path_hooks 中的每個可調(diào)用對象。 對此列表中的每個 路徑條目鉤子 的調(diào)用會帶有一個參數(shù),即要搜索的路徑條目。 每個可調(diào)用對象或是返回可處理路徑條目的 path entry finder,或是引發(fā) ImportError。 基于路徑的查找器使用 ImportError 來表示鉤子無法找到與 path entry 相對應的 path entry finder。 該異常會被忽略并繼續(xù)進行 import path 的迭代。 每個鉤子應該期待接收一個字符串或字節(jié)串對象;字節(jié)串對象的編碼由鉤子決定(例如可以是文件系統(tǒng)使用的編碼 UTF-8 或其它編碼),如果鉤子無法解碼參數(shù),它應該引發(fā) ImportError

如果 sys.path_hooks 迭代結束時沒有返回 path entry finder,則基于路徑的查找器 find_spec() 方法將在 sys.path_importer_cache 中存入 None (表示此路徑條目沒有對應的查找器) 并返回 None,表示此 meta path finder 無法找到該模塊。

如果 sys.path_hooks 中的某個 path entry hook 可調(diào)用對象的返回值 一個 path entry finder,則以下協(xié)議會被用來向查找器請求一個模塊的規(guī)格說明,并在加載該模塊時被使用。

當前工作目錄 -- 由一個空字符串表示 -- 的處理方式與 sys.path 中的其他條目略有不同。 首先,如果發(fā)現(xiàn)當前工作目錄不存在,則 sys.path_importer_cache 中不會存放任何值。 其次,每個模塊查找會對當前工作目錄的值進行全新查找。 第三,由 sys.path_importer_cache 所使用并由 importlib.machinery.PathFinder.find_spec() 所返回的路徑將是實際的當前工作目錄而非空字符串。

5.5.2. 路徑條目查找器協(xié)議?

為了支持模塊和已初始化包的導入,也為了給命名空間包提供組成部分,路徑條目查找器必須實現(xiàn) find_spec() 方法。

find_spec() 接受兩個參數(shù),即要導入模塊的完整限定名稱,以及(可選的)目標模塊。 find_spec() 返回模塊的完全填充好的規(guī)格說明。 這個規(guī)格說明總是包含“加載器”集合(但有一個例外)。

為了向導入機制提示該規(guī)格說明代表一個命名空間 portion,路徑條目查找器會將 "submodule_search_locations" 設為一個包含該部分的列表。

在 3.4 版更改: find_spec() 替代了 find_loader()find_module(),后兩者現(xiàn)在都已棄用,但會在 find_spec() 未定義時被使用。

較舊的路徑條目查找器可能會實現(xiàn)這兩個已棄用的方法中的一個而沒有實現(xiàn) find_spec()。 為保持向后兼容,這兩個方法仍會被接受。 但是,如果在路徑條目查找器上實現(xiàn)了 find_spec(),這兩個遺留方法就會被忽略。

find_loader() 接受一個參數(shù),即要導入模塊的完整限定名稱。 find_loader() 返回一個 2 元組,其中第一項是加載器而第二項是命名空間 portion

為了向后兼容其他導入?yún)f(xié)議的實現(xiàn),許多路徑條目查找器也同樣支持元路徑查找器所支持的傳統(tǒng) find_module() 方法。 但是路徑條目查找器 find_module() 方法的調(diào)用絕不會帶有 path 參數(shù)(它們被期望記錄來自對路徑鉤子初始調(diào)用的恰當路徑信息)。

路徑條目查找器的 find_module() 方法已棄用,因為它不允許路徑條目查找器為命名空間包提供部分。 如果 find_loader()find_module() 同時存在于一個路徑條目查找器中,導入系統(tǒng)將總是調(diào)用 find_loader() 而不選擇 find_module()

在 3.10 版更改: 導入系統(tǒng)調(diào)用 find_module()find_loader() 將引發(fā) ImportWarning。

5.6. 替換標準導入系統(tǒng)?

替換整個導入系統(tǒng)的最可靠機制是移除 sys.meta_path 的默認內(nèi)容,,將其完全替換為自定義的元路徑鉤子。

一個可行的方式是僅改變導入語句的行為而不影響訪問導入系統(tǒng)的其他 API,那么替換內(nèi)置的 __import__() 函數(shù)可能就夠了。 這種技巧也可以在模塊層級上運用,即只在某個模塊內(nèi)部改變導入語句的行為。

想要選擇性地預先防止在元路徑上從一個鉤子導入某些模塊(而不是完全禁用標準導入系統(tǒng)),只需直接從 find_spec() 引發(fā) ModuleNotFoundError 而非返回 None 就足夠了。 返回后者表示元路徑搜索應當繼續(xù),而引發(fā)異常則會立即終止搜索。

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 的導入系統(tǒng)來說 __main__ 模塊是一個特殊情況。 正如在 另一節(jié) 中所述,__main__ 模塊是在解釋器啟動時直接初始化的,與 sysbuiltins 很類似。 但是,與那兩者不同,它并不被嚴格歸類為內(nèi)置模塊。 這是因為 __main__ 被初始化的方式依賴于發(fā)起調(diào)用解釋器所附帶的旗標和其他選項。

5.8.1. __main__.__spec__?

根據(jù) __main__ 被初始化的方式,__main__.__spec__ 會被設置相應值或是 None

當 Python 附加 -m 選項啟動時,__spec__ 會被設為相應模塊或包的模塊規(guī)格說明。 __spec__ 也會在 __main__ 模塊作為執(zhí)行某個目錄,zip 文件或其它 sys.path 條目的一部分加載時被填充。

其余的情況__main__.__spec__ 會被設為 None,因為用于填充 __main__ 的代碼不直接與可導入的模塊相對應:

  • 交互型提示

  • -c 選項

  • 從 stdin 運行

  • 直接從源碼或字節(jié)碼文件運行

請注意在最后一種情況中 __main__.__spec__ 總是為 None,即使 文件從技術上說可以作為一個模塊被導入。 如果想要讓 __main__ 中的元數(shù)據(jù)生效,請使用 -m 開關。

還要注意即使是在 __main__ 對應于一個可導入模塊且 __main__.__spec__ 被相應地設定時,它們?nèi)詴灰暈?不同的 模塊。 這是由于以下事實:使用 if __name__ == "__main__": 檢測來保護的代碼塊僅會在模塊被用來填充 __main__ 命名空間時而非普通的導入時被執(zhí)行。

5.9. 開放問題項?

XXX 最好是能增加一個圖表。

XXX * (import_machinery.rst) 是否要專門增加一節(jié)來說明模塊和包的屬性,也許可以擴展或移植數(shù)據(jù)模型參考頁中的相關條目?

XXX 庫手冊中的 runpy 和 pkgutil 等等應該都在頁面頂端增加指向新的導入系統(tǒng)章節(jié)的“另請參閱”鏈接。

XXX 是否要增加關于初始化 __main__ 的不同方式的更多解釋?

XXX 增加更多有關 __main__ 怪異/坑人特性的信息 (例如直接從 PEP 395 復制)。

5.10. 參考文獻?

導入機制自 Python 誕生之初至今已發(fā)生了很大的變化。 原始的 包規(guī)格說明 仍然可以查閱,但在撰寫該文檔之后許多相關細節(jié)已被修改。

原始的 sys.meta_path 規(guī)格說明見 PEP 302,后續(xù)的擴展說明見 PEP 420

PEP 420 為 Python 3.3 引入了 命名空間包。 PEP 420 還引入了 find_loader() 協(xié)議作為 find_module() 的替代。

PEP 366 描述了新增的 __package__ 屬性,用于在模塊中的顯式相對導入。

PEP 328 引入了絕對和顯式相對導入,并初次提出了 __name__ 語義,最終由 PEP 366__package__ 加入規(guī)范描述。

PEP 338 定義了將模塊作為腳本執(zhí)行。

PEP 451 在 spec 對象中增加了對每個模塊導入狀態(tài)的封裝。 它還將加載器的大部分樣板責任移交回導入機制中。 這些改變允許棄用導入系統(tǒng)中的一些 API 并為查找器和加載器增加一些新的方法。

備注

1

參見 types.ModuleType。

2

importlib 實現(xiàn)避免直接使用返回值。 而是通過在 sys.modules 中查找模塊名稱來獲取模塊對象。 這種方式的間接影響是被導入的模塊可能在 sys.modules 中替換其自身。 這屬于具體實現(xiàn)的特定行為,不保證能在其他 Python 實現(xiàn)中起作用。

3

在遺留代碼中,有可能在 sys.path_importer_cache 中找到 imp.NullImporter 的實例。 建議將這些代碼修改為使用 None 代替。 詳情參見 Porting Python code。