5. 導入系統?

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

import 語(yǔ)句結合了兩個(gè)操作;它先搜索指定名稱(chēng)的模塊,然后將搜索結果綁定到當前作用域中的名稱(chēng)。 import 語(yǔ)句的搜索操作被定義為對 __import__() 函數的調用并帶有適當的參數。 __import__() 的返回值會(huì )被用于執行 import 語(yǔ)句的名稱(chēng)綁定操作。 請參閱 import 語(yǔ)句了解名稱(chēng)綁定操作的更多細節。

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

import 語(yǔ)句被執行時(shí),標準的內置 __import__() 函數會(huì )被調用。 其他發(fā)起調用導入系統的機制 (例如 importlib.import_module()) 可能會(huì )選擇繞過(guò) __import__() 并使用它們自己的解決方案來(lái)實(shí)現導入機制。

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

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

5.1. importlib?

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

5.2. ?

Python 只有一種模塊對象類(lèi)型,所有模塊都屬于該類(lèi)型,無(wú)論模塊是用 Python、C 還是別的語(yǔ)言實(shí)現。 為了幫助組織模塊并提供名稱(chēng)層次結構,Python 還引入了 的概念。

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

要注意的一個(gè)重點(diǎn)概念是所有包都是模塊,但并非所有模塊都是包。 或者換句話(huà)說(shuō),包只是一種特殊的模塊。 特別地,任何具有 __path__ 屬性的模塊都會(huì )被當作是包。

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. 常規包?

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

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

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。

5.2.2. 命名空間包?

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

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

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

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

5.3. 搜索?

為了開(kāi)始搜索,Python 需要被導入模塊(或者包,對于當前討論來(lái)說(shuō)兩者沒(méi)有差別)的完整 限定名稱(chēng)。 此名稱(chēng)可以來(lái)自 import 語(yǔ)句所帶的各種參數,或者來(lái)自傳給 importlib.import_module()__import__() 函數的形參。

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

5.3.1. 模塊緩存?

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

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

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

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

5.3.2. 查找器和加載器?

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

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

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

查找器并不真正加載模塊。 如果它們能找到指定名稱(chēng)的模塊,會(huì )返回一個(gè) 模塊規格說(shuō)明,這是對模塊導入相關(guān)信息的封裝,供后續導入機制用于在加載模塊時(shí)使用。

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

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

5.3.3. 導入鉤子?

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

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

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

5.3.4. 元路徑?

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

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

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

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

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

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

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

在 3.10 版更改: 導入系統使用 find_module() 現在將會(huì )引發(fā) ImportWarning。

5.4. 加載?

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

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]

請注意以下細節:

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

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

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

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

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

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

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

5.4.1. 加載器?

模塊加載器提供關(guān)鍵的加載功能:模塊執行。 導入機制調用 importlib.abc.Loader.exec_module() 方法并傳入一個(gè)參數來(lái)執行模塊對象。 從 exec_module() 返回的任何值都將被忽略。

加載器必須滿(mǎn)足下列要求:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.4.2. 子模塊?

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

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 名稱(chēng)綁定規則,這看起來(lái)可能會(huì )令人驚訝,但它實(shí)際上是導入系統的一個(gè)基本特性。 保持不變的一點(diǎn)是如果你有 sys.modules['spam']sys.modules['spam.foo'] (例如在上述導入之后就是如此),則后者必須顯示為前者的 foo 屬性。

5.4.3. 模塊規格說(shuō)明?

導入機制在導入期間會(huì )使用有關(guān)每個(gè)模塊的多種信息,特別是加載之前。 大多數信息都是所有模塊通用的。 模塊規格說(shuō)明的目的是基于每個(gè)模塊來(lái)封裝這些導入相關(guān)信息。

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

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

3.4 新版功能.

5.4.5. module.__path__?

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

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

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

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

5.4.6. 模塊的 repr?

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

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

以下是所使用的確切規則:

  • 如果模塊具有 __spec__ 屬性,其中的規格信息會(huì )被用來(lái)生成 repr。 被查詢(xún)的屬性有 "name", "loader", "origin" 和 "has_location" 等等。

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

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

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

在 3.4 版更改: loader.module_repr() 已棄用,導入機制現在使用模塊規格說(shuō)明來(lái)生成模塊 repr。

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

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

5.4.7. 已緩存字節碼的失效?

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

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

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

5.5. 基于路徑的查找器?

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

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

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

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

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

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

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

5.5.1. 路徑條目查找器?

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

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

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

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

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

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

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

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

如果 sys.path_hooks 中的某個(gè) path entry hook 可調用對象的返回值 一個(gè) path entry finder,則以下協(xié)議會(huì )被用來(lái)向查找器請求一個(gè)模塊的規格說(shuō)明,并在加載該模塊時(shí)被使用。

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

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

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

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

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

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

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

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

為了向后兼容其他導入協(xié)議的實(shí)現,許多路徑條目查找器也同樣支持元路徑查找器所支持的傳統 find_module() 方法。 但是路徑條目查找器 find_module() 方法的調用絕不會(huì )帶有 path 參數(它們被期望記錄來(lái)自對路徑鉤子初始調用的恰當路徑信息)。

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

在 3.10 版更改: 導入系統調用 find_module()find_loader() 將引發(fā) ImportWarning。

5.6. 替換標準導入系統?

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

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

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

5.7. 包相對導入?

相對導入使用前綴點(diǎn)號。 一個(gè)前綴點(diǎn)號表示相對導入從當前包開(kāi)始。 兩個(gè)或更多前綴點(diǎn)號表示對當前包的上級包的相對導入,第一個(gè)點(diǎn)號之后的每個(gè)點(diǎn)號代表一級。 例如,給定以下的包布局結構:

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 <> 語(yǔ)法,但相對導入只能使用第二種形式;其中的原因在于:

import XXX.YYY.ZZZ

應當提供 XXX.YYY.ZZZ 作為可用表達式,但 .moduleY 不是一個(gè)有效的表達式。

5.8. 有關(guān) __main__ 的特殊事項?

對于 Python 的導入系統來(lái)說(shuō) __main__ 模塊是一個(gè)特殊情況。 正如在 另一節 中所述,__main__ 模塊是在解釋器啟動(dòng)時(shí)直接初始化的,與 sysbuiltins 很類(lèi)似。 但是,與那兩者不同,它并不被嚴格歸類(lèi)為內置模塊。 這是因為 __main__ 被初始化的方式依賴(lài)于發(fā)起調用解釋器所附帶的旗標和其他選項。

5.8.1. __main__.__spec__?

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

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

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

  • 交互型提示

  • -c 選項

  • 從 stdin 運行

  • 直接從源碼或字節碼文件運行

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

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

5.9. 開(kāi)放問(wèn)題項?

XXX 最好是能增加一個(gè)圖表。

XXX * (import_machinery.rst) 是否要專(zhuān)門(mén)增加一節來(lái)說(shuō)明模塊和包的屬性,也許可以擴展或移植數據模型參考頁(yè)中的相關(guān)條目?

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

XXX 是否要增加關(guān)于初始化 __main__ 的不同方式的更多解釋?zhuān)?/p>

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

5.10. 參考文獻?

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

原始的 sys.meta_path 規格說(shuō)明見(jiàn) PEP 302,后續的擴展說(shuō)明見(jiàn) PEP 420。

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

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

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

PEP 338 定義了將模塊作為腳本執行。

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

備注

1

參見(jiàn) types.ModuleType。

2

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

3

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