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

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

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

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

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

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

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

5.1. importlib?

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

5.2. ?

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

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

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

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ī)包通常以一個(gè)包含 __init__.py 文件的目錄形式實(shí)現(xiàn)。 當(dāng)一個(gè)常規(guī)包被導(dǎo)入時(shí),這個(gè) __init__.py 文件會(huì)隱式地被執(zhí)行,它所定義的對象會(huì)被綁定到該包命名空間中的名稱。__init__.py 文件可以包含與任何其他模塊中所包含的 Python 代碼相似的代碼,Python 將在模塊被導(dǎo)入時(shí)為其添加額外的屬性。

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

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

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

5.2.2. 命名空間包?

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

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

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

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

5.3. 搜索?

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

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

5.3.1. 模塊緩存?

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

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

sys.modules 是可寫的。刪除鍵可能不會(huì)破壞關(guān)聯(lián)的模塊(因?yàn)槠渌K可能會(huì)保留對它的引用),但它會(huì)使命名模塊的緩存條目無效,導(dǎo)致 Python 在下次導(dǎo)入時(shí)重新搜索命名模塊。鍵也可以賦值為 None ,強(qiáng)制下一次導(dǎo)入模塊導(dǎo)致 ModuleNotFoundError

但是要小心,因?yàn)槿绻氵€保有對某個(gè)模塊對象的引用,同時(shí)停用其在 sys.modules 中的緩存條目,然后又再次導(dǎo)入該名稱的模塊,則前后兩個(gè)模塊對象將 不是 同一個(gè)。 相反地,importlib.reload() 將重用 同一個(gè) 模塊對象,并簡單地通過重新運(yùn)行模塊的代碼來重新初始化模塊內(nèi)容。

5.3.2. 查找器和加載器?

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

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

導(dǎo)入機(jī)制是可擴(kuò)展的,因此可以加入新的查找器以擴(kuò)展模塊搜索的范圍和作用域。

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

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

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

5.3.3. 導(dǎo)入鉤子?

導(dǎo)入機(jī)制被設(shè)計(jì)為可擴(kuò)展;其中的基本機(jī)制是 導(dǎo)入鉤子。 導(dǎo)入鉤子有兩種類型: 元鉤子導(dǎo)入路徑鉤子。

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

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

5.3.4. 元路徑?

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

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

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

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

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

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

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

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

5.4. 加載?

當(dāng)一個(gè)模塊說明被找到時(shí),導(dǎo)入機(jī)制將在加載該模塊時(shí)使用它(及其所包含的加載器)。 下面是導(dǎo)入的加載部分所發(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]

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

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

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

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

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

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

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

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

5.4.1. 加載器?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.4.2. 子模塊?

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

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

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

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

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

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

3.4 新版功能.

5.4.5. module.__path__?

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

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

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

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

5.4.6. 模塊的 repr?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.5. 基于路徑的查找器?

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

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

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

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

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

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

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

5.5.1. 路徑條目查找器?

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

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

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

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

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

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

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

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

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

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

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

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

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

為了向?qū)霗C(jī)制提示該規(guī)格說明代表一個(gè)命名空間 portion,路徑條目查找器會(huì)將 "submodule_search_locations" 設(shè)為一個(gè)包含該部分的列表。

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

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

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

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

路徑條目查找器的 find_module() 方法已棄用,因?yàn)樗辉试S路徑條目查找器為命名空間包提供部分。 如果 find_loader()find_module() 同時(shí)存在于一個(gè)路徑條目查找器中,導(dǎo)入系統(tǒng)將總是調(diào)用 find_loader() 而不選擇 find_module()

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

5.6. 替換標(biāo)準(zhǔn)導(dǎo)入系統(tǒng)?

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

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

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

5.7. 包相對導(dǎo)入?

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

package/
    __init__.py
    subpackage1/
        __init__.py
        moduleX.py
        moduleY.py
    subpackage2/
        __init__.py
        moduleZ.py
    moduleA.py

不論是在 subpackage1/moduleX.py 還是 subpackage1/__init__.py 中,以下導(dǎo)入都是有效的:

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

絕對導(dǎo)入可以使用 import <>from <> import <> 語法,但相對導(dǎo)入只能使用第二種形式;其中的原因在于:

import XXX.YYY.ZZZ

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

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

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

5.8.1. __main__.__spec__?

根據(jù) __main__ 被初始化的方式,__main__.__spec__ 會(huì)被設(shè)置相應(yīng)值或是 None。

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

其余的情況__main__.__spec__ 會(huì)被設(shè)為 None,因?yàn)橛糜谔畛?__main__ 的代碼不直接與可導(dǎo)入的模塊相對應(yīng):

  • 交互型提示

  • -c 選項(xiàng)

  • 從 stdin 運(yùn)行

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

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

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

5.9. 開放問題項(xiàng)?

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

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

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

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

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

5.10. 參考文獻(xiàn)?

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

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

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

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

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

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

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

備注

1

參見 types.ModuleType

2

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

3

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