5. 導(dǎo)入系統(tǒng)?
一個 module 內(nèi)的 Python 代碼通過 importing 操作就能夠訪問另一個模塊內(nèi)的代碼。 import
語句是發(fā)起調(diào)用導(dǎo)入機制的最常用方式,但不是唯一的方式。 importlib.import_module()
以及內(nèi)置的 __import__()
等函數(shù)也可以被用來發(fā)起調(diào)用導(dǎo)入機制。
import
語句結(jié)合了兩個操作;它先搜索指定名稱的模塊,然后將搜索結(jié)果綁定到當(dāng)前作用域中的名稱。 import
語句的搜索操作被定義為對 __import__()
函數(shù)的調(diào)用并帶有適當(dāng)?shù)膮?shù)。 __import__()
的返回值會被用于執(zhí)行 import
語句的名稱綁定操作。 請參閱 import
語句了解名稱綁定操作的更多細節(jié)。
對 __import__()
的直接調(diào)用將僅執(zhí)行模塊搜索以及在找到時的模塊創(chuàng)建操作。 不過也可能產(chǎn)生某些副作用,例如導(dǎo)入父包和更新各種緩存 (包括 sys.modules
),只有 import
語句會執(zhí)行名稱綁定操作。
當(dāng) import
語句被執(zhí)行時,標(biāo)準(zhǔn)的內(nèi)置 __import__()
函數(shù)會被調(diào)用。 其他發(fā)起調(diào)用導(dǎo)入系統(tǒng)的機制 (例如 importlib.import_module()
) 可能會選擇繞過 __import__()
并使用它們自己的解決方案來實現(xiàn)導(dǎo)入機制。
當(dāng)一個模塊首次被導(dǎo)入時,Python 會搜索該模塊,如果找到就創(chuàng)建一個 module 對象 1 并初始化它。 如果指定名稱的模塊未找到,則會引發(fā) ModuleNotFoundError
。 當(dāng)發(fā)起調(diào)用導(dǎo)入機制時,Python 會實現(xiàn)多種策略來搜索指定名稱的模塊。 這些策略可以通過使用使用下文所描述的多種鉤子來加以修改和擴展。
在 3.3 版更改: 導(dǎo)入系統(tǒng)已被更新以完全實現(xiàn) PEP 302 中的第二階段要求。 不會再有任何隱式的導(dǎo)入機制 —— 整個導(dǎo)入系統(tǒng)都通過 sys.meta_path
暴露出來。 此外,對原生命名空間包的支持也已被實現(xiàn) (參見 PEP 420)。
5.1. importlib
?
importlib
模塊提供了一個豐富的 API 用來與導(dǎo)入系統(tǒng)進行交互。 例如 importlib.import_module()
提供了相比內(nèi)置的 __import__()
更推薦、更簡單的 API 用來發(fā)起調(diào)用導(dǎo)入機制。 更多細節(jié)請參看 importlib
庫文檔。
5.2. 包?
Python 只有一種模塊對象類型,所有模塊都屬于該類型,無論模塊是用 Python、C 還是別的語言實現(xiàn)。 為了幫助組織模塊并提供名稱層次結(jié)構(gòu),Python 還引入了 包 的概念。
你可以把包看成是文件系統(tǒng)中的目錄,并把模塊看成是目錄中的文件,但請不要對這個類比做過于字面的理解,因為包和模塊不是必須來自于文件系統(tǒng)。 為了方便理解本文檔,我們將繼續(xù)使用這種目錄和文件的類比。 與文件系統(tǒng)一樣,包通過層次結(jié)構(gòu)進行組織,在包之內(nèi)除了一般的模塊,還可以有子包。
要注意的一個重點概念是所有包都是模塊,但并非所有模塊都是包。 或者換句話說,包只是一種特殊的模塊。 特別地,任何具有 __path__
屬性的模塊都會被當(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ī)包通常以一個包含 __init__.py
文件的目錄形式實現(xiàn)。 當(dāng)一個常規(guī)包被導(dǎo)入時,這個 __init__.py
文件會隱式地被執(zhí)行,它所定義的對象會被綁定到該包命名空間中的名稱。__init__.py
文件可以包含與任何其他模塊中所包含的 Python 代碼相似的代碼,Python 將在模塊被導(dǎo)入時為其添加額外的屬性。
例如,以下文件系統(tǒng)布局定義了一個最高層級的 parent
包和三個子包:
parent/
__init__.py
one/
__init__.py
two/
__init__.py
three/
__init__.py
導(dǎo)入 parent.one
將隱式地執(zhí)行 parent/__init__.py
和 parent/one/__init__.py
。 后續(xù)導(dǎo)入 parent.two
或 parent.three
則將分別執(zhí)行 parent/two/__init__.py
和 parent/three/__init__.py
。
5.2.2. 命名空間包?
命名空間包是由多個 部分 構(gòu)成的,每個部分為父包增加一個子包。 各個部分可能處于文件系統(tǒng)的不同位置。 部分也可能處于 zip 文件中、網(wǎng)絡(luò)上,或者 Python 在導(dǎo)入期間可以搜索的其他地方。 命名空間包并不一定會直接對應(yīng)到文件系統(tǒng)中的對象;它們有可能是無實體表示的虛擬模塊。
命名空間包的 __path__
屬性不使用普通的列表。 而是使用定制的可迭代類型,如果其父包的路徑 (或者最高層級包的 sys.path
) 發(fā)生改變,這種對象會在該包內(nèi)的下一次導(dǎo)入嘗試時自動執(zhí)行新的對包部分的搜索。
命名空間包沒有 parent/__init__.py
文件。 實際上,在導(dǎo)入搜索期間可能找到多個 parent
目錄,每個都由不同的部分所提供。 因此 parent/one
的物理位置不一定與 parent/two
相鄰。 在這種情況下,Python 將為頂級的 parent
包創(chuàng)建一個命名空間包,無論是它本身還是它的某個子包被導(dǎo)入。
另請參閱 PEP 420 了解對命名空間包的規(guī)格描述。
5.3. 搜索?
為了開始搜索,Python 需要被導(dǎo)入模塊(或者包,對于當(dāng)前討論來說兩者沒有差別)的完整 限定名稱。 此名稱可以來自 import
語句所帶的各種參數(shù),或者來自傳給 importlib.import_module()
或 __import__()
函數(shù)的形參。
此名稱會在導(dǎo)入搜索的各個階段被使用,它也可以是指向一個子模塊的帶點號路徑,例如 foo.bar.baz
。 在這種情況下,Python 會先嘗試導(dǎo)入 foo
,然后是 foo.bar
,最后是 foo.bar.baz
。 如果這些導(dǎo)入中的任何一個失敗,都會引發(fā) ModuleNotFoundError
。
5.3.1. 模塊緩存?
在導(dǎo)入搜索期間首先會被檢查的地方是 sys.modules
。 這個映射起到緩存之前導(dǎo)入的所有模塊的作用(包括其中間路徑)。 因此如果之前導(dǎo)入過 foo.bar.baz
,則 sys.modules
將包含 foo
, foo.bar
和 foo.bar.baz
條目。 每個鍵的值就是相應(yīng)的模塊對象。
在導(dǎo)入期間,會在 sys.modules
查找模塊名稱,如存在則其關(guān)聯(lián)的值就是需要導(dǎo)入的模塊,導(dǎo)入過程完成。 然而,如果值為 None
,則會引發(fā) ModuleNotFoundError
。 如果找不到指定模塊名稱,Python 將繼續(xù)搜索該模塊。
sys.modules
是可寫的。刪除鍵可能不會破壞關(guān)聯(lián)的模塊(因為其他模塊可能會保留對它的引用),但它會使命名模塊的緩存條目無效,導(dǎo)致 Python 在下次導(dǎo)入時重新搜索命名模塊。鍵也可以賦值為 None
,強制下一次導(dǎo)入模塊導(dǎo)致 ModuleNotFoundError
。
但是要小心,因為如果你還保有對某個模塊對象的引用,同時停用其在 sys.modules
中的緩存條目,然后又再次導(dǎo)入該名稱的模塊,則前后兩個模塊對象將 不是 同一個。 相反地,importlib.reload()
將重用 同一個 模塊對象,并簡單地通過重新運行模塊的代碼來重新初始化模塊內(nèi)容。
5.3.2. 查找器和加載器?
如果指定名稱的模塊在 sys.modules
找不到,則將發(fā)起調(diào)用 Python 的導(dǎo)入?yún)f(xié)議以查找和加載該模塊。 此協(xié)議由兩個概念性模塊構(gòu)成,即 查找器 和 加載器。 查找器的任務(wù)是確定是否能使用其所知的策略找到該名稱的模塊。 同時實現(xiàn)這兩種接口的對象稱為 導(dǎo)入器 —— 它們在確定能加載所需的模塊時會返回其自身。
Python 包含了多個默認(rèn)查找器和導(dǎo)入器。 第一個知道如何定位內(nèi)置模塊,第二個知道如何定位凍結(jié)模塊。 第三個默認(rèn)查找器會在 import path 中搜索模塊。 import path 是一個由文件系統(tǒng)路徑或 zip 文件組成的位置列表。 它還可以擴展為搜索任意可定位資源,例如由 URL 指定的資源。
導(dǎo)入機制是可擴展的,因此可以加入新的查找器以擴展模塊搜索的范圍和作用域。
查找器并不真正加載模塊。 如果它們能找到指定名稱的模塊,會返回一個 模塊規(guī)格說明,這是對模塊導(dǎo)入相關(guān)信息的封裝,供后續(xù)導(dǎo)入機制用于在加載模塊時使用。
以下各節(jié)描述了有關(guān)查找器和加載器協(xié)議的更多細節(jié),包括你應(yīng)該如何創(chuàng)建并注冊新的此類對象來擴展導(dǎo)入機制。
在 3.4 版更改: 在之前的 Python 版本中,查找器會直接返回 加載器,現(xiàn)在它們則返回模塊規(guī)格說明,其中 包含 加載器。 加載器仍然在導(dǎo)入期間被使用,但負擔(dān)的任務(wù)有所減少。
5.3.3. 導(dǎo)入鉤子?
導(dǎo)入機制被設(shè)計為可擴展;其中的基本機制是 導(dǎo)入鉤子。 導(dǎo)入鉤子有兩種類型: 元鉤子 和 導(dǎo)入路徑鉤子。
元鉤子在導(dǎo)入過程開始時被調(diào)用,此時任何其他導(dǎo)入過程尚未發(fā)生,但 sys.modules
緩存查找除外。 這允許元鉤子重載 sys.path
過程、凍結(jié)模塊甚至內(nèi)置模塊。 元鉤子的注冊是通過向 sys.meta_path
添加新的查找器對象,具體如下所述。
導(dǎo)入路徑鉤子是作為 sys.path
(或 package.__path__
) 過程的一部分,在遇到它們所關(guān)聯(lián)的路徑項的時候被調(diào)用。 導(dǎo)入路徑鉤子的注冊是通過向 sys.path_hooks
添加新的可調(diào)用對象,具體如下所述。
5.3.4. 元路徑?
當(dāng)指定名稱的模塊在 sys.modules
中找不到時,Python 會接著搜索 sys.meta_path
,其中包含元路徑查找器對象列表。 這些查找器按順序被查詢以確定它們是否知道如何處理該名稱的模塊。 元路徑查找器必須實現(xiàn)名為 find_spec()
的方法,該方法接受三個參數(shù):名稱、導(dǎo)入路徑和目標(biāo)模塊(可選)。 元路徑查找器可使用任何策略來確定它是否能處理指定名稱的模塊。
如果元路徑查找器知道如何處理指定名稱的模塊,它將返回一個說明對象。 如果它不能處理該名稱的模塊,則會返回 None
。 如果 sys.meta_path
處理過程到達列表末尾仍未返回說明對象,則將引發(fā) ModuleNotFoundError
。 任何其他被引發(fā)異常將直接向上傳播,并放棄導(dǎo)入過程。
元路徑查找器的 find_spec()
方法調(diào)用帶有兩到三個參數(shù)。 第一個是被導(dǎo)入模塊的完整限定名稱,例如 foo.bar.baz
。 第二個參數(shù)是供模塊搜索使用的路徑條目。 對于最高層級模塊,第二個參數(shù)為 None
,但對于子模塊或子包,第二個參數(shù)為父包 __path__
屬性的值。 如果相應(yīng)的 __path__
屬性無法訪問,將引發(fā) ModuleNotFoundError
。 第三個參數(shù)是一個將被作為稍后加載目標(biāo)的現(xiàn)有模塊對象。 導(dǎo)入系統(tǒng)僅會在重加載期間傳入一個目標(biāo)模塊。
對于單個導(dǎo)入請求可以多次遍歷元路徑。 例如,假設(shè)所涉及的模塊都尚未被緩存,則導(dǎo)入 foo.bar.baz
將首先執(zhí)行頂級的導(dǎo)入,在每個元路徑查找器 (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
以外的對象作為第三個參數(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ù)可用但不會再做改變,導(dǎo)入機制僅會在查找器未實現(xiàn) find_spec()
時嘗試使用它。
在 3.10 版更改: 導(dǎo)入系統(tǒng)使用 find_module()
現(xiàn)在將會引發(fā) ImportWarning
。
5.4. 加載?
當(dāng)一個模塊說明被找到時,導(dǎo)入機制將在加載該模塊時使用它(及其所包含的加載器)。 下面是導(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]
請注意以下細節(jié):
如果在
sys.modules
中存在指定名稱的模塊對象,導(dǎo)入操作會已經(jīng)將其返回。在加載器執(zhí)行模塊代碼之前,該模塊將存在于
sys.modules
中。 這一點很關(guān)鍵,因為該模塊代碼可能(直接或間接地)導(dǎo)入其自身;預(yù)先將其添加到sys.modules
可防止在最壞情況下的無限遞歸和最好情況下的多次加載。如果加載失敗,則該模塊 -- 只限加載失敗的模塊 -- 將從
sys.modules
中移除。 任何已存在于sys.modules
緩存的模塊,以及任何作為附帶影響被成功加載的模塊仍會保留在緩存中。 這與重新加載不同,后者會把即使加載失敗的模塊也保留在sys.modules
中。在模塊創(chuàng)建完成但還未執(zhí)行之前,導(dǎo)入機制會設(shè)置導(dǎo)入相關(guān)模塊屬性(在上面的示例偽代碼中為 “_init_module_attrs”),詳情參見 后續(xù)部分。
模塊執(zhí)行是加載的關(guān)鍵時刻,在此期間將填充模塊的命名空間。 執(zhí)行會完全委托給加載器,由加載器決定要填充的內(nèi)容和方式。
在加載過程中創(chuàng)建并傳遞給 exec_module() 的模塊并不一定就是在導(dǎo)入結(jié)束時返回的模塊 2。
在 3.4 版更改: 導(dǎo)入系統(tǒng)已經(jīng)接管了加載器建立樣板的責(zé)任。 這些在以前是由 importlib.abc.Loader.load_module()
方法來執(zhí)行的。
5.4.1. 加載器?
模塊加載器提供關(guān)鍵的加載功能:模塊執(zhí)行。 導(dǎo)入機制調(diào)用 importlib.abc.Loader.exec_module()
方法并傳入一個參數(shù)來執(zhí)行模塊對象。 從 exec_module()
返回的任何值都將被忽略。
加載器必須滿足下列要求:
如果模塊是一個 Python 模塊(而非內(nèi)置模塊或動態(tài)加載的擴展),加載器應(yīng)該在模塊的全局命名空間 (
module.__dict__
) 中執(zhí)行模塊的代碼。如果加載器無法執(zhí)行指定模塊,它應(yīng)該引發(fā)
ImportError
,不過在exec_module()
期間引發(fā)的任何其他異常也會被傳播。
在許多情況下,查找器和加載器可以是同一對象;在此情況下 find_spec()
方法將返回一個規(guī)格說明,其中加載器會被設(shè)為 self
。
模塊加載器可以選擇通過實現(xiàn) create_module()
方法在加載期間創(chuàng)建模塊對象。 它接受一個參數(shù),即模塊規(guī)格說明,并返回新的模塊對象供加載期間使用。 create_module()
不需要在模塊對象上設(shè)置任何屬性。 如果模塊返回 None
,導(dǎo)入機制將自行創(chuàng)建新模塊。
3.4 新版功能: 加載器的 create_module()
方法。
在 3.4 版更改: load_module()
方法被 exec_module()
所替代,導(dǎo)入機制會對加載的所有樣板責(zé)任作出假定。
為了與現(xiàn)有的加載器兼容,導(dǎo)入機制會使用導(dǎo)入器的 load_module()
方法,如果它存在且導(dǎo)入器也未實現(xiàn) exec_module()
。 但是,load_module()
現(xiàn)已棄用,加載器應(yīng)該轉(zhuǎ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
中的模塊,但它必須 僅限 移除加載失敗的模塊,且所移除的模塊應(yīng)為加載器自身顯式加載的。
在 3.5 版更改: 當(dāng) exec_module()
已定義但 create_module()
未定義時將引發(fā) DeprecationWarning
。
在 3.6 版更改: 當(dāng) exec_module()
已定義但 create_module()
未定義時將引發(fā) ImportError
。
在 3.10 版更改: 使用 load_module()
將引發(fā) ImportWarning
。
5.4.2. 子模塊?
當(dāng)使用任意機制 (例如 importlib
API, import
及 import-from
語句或者內(nèi)置的 __import__()
) 加載一個子模塊時,父模塊的命名空間中會添加一個對子模塊對象的綁定。 例如,如果包 spam
有一個子模塊 foo
,則在導(dǎo)入 spam.foo
之后,spam
將具有一個 綁定到相應(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ī)則,這看起來可能會令人驚訝,但它實際上是導(dǎo)入系統(tǒng)的一個基本特性。 保持不變的一點是如果你有 sys.modules['spam']
和 sys.modules['spam.foo']
(例如在上述導(dǎo)入之后就是如此),則后者必須顯示為前者的 foo
屬性。
5.4.3. 模塊規(guī)格說明?
導(dǎo)入機制在導(dǎo)入期間會使用有關(guān)每個模塊的多種信息,特別是加載之前。 大多數(shù)信息都是所有模塊通用的。 模塊規(guī)格說明的目的是基于每個模塊來封裝這些導(dǎo)入相關(guān)信息。
在導(dǎo)入期間使用規(guī)格說明可允許狀態(tài)在導(dǎo)入系統(tǒng)各組件之間傳遞,例如在創(chuàng)建模塊規(guī)格說明的查找器和執(zhí)行模塊的加載器之間。 最重要的一點是,它允許導(dǎo)入機制執(zhí)行加載的樣板操作,在沒有模塊規(guī)格說明的情況下這是加載器的責(zé)任。
模塊的規(guī)格說明會作為模塊對象的 __spec__
屬性對外公開。 有關(guān)模塊規(guī)格的詳細內(nèi)容請參閱 ModuleSpec
。
3.4 新版功能.
5.4.5. module.__path__?
根據(jù)定義,如果一個模塊具有 __path__
屬性,它就是包。
包的 __path__
屬性會在導(dǎo)入其子包期間被使用。 在導(dǎo)入機制內(nèi)部,它的功能與 sys.path
基本相同,即在導(dǎo)入期間提供一個模塊搜索位置列表。 但是,__path__
通常會比 sys.path
受到更多限制。
__path__
必須是由字符串組成的可迭代對象,但它也可以為空。 作用于 sys.path
的規(guī)則同樣適用于包的 __path__
,并且 sys.path_hooks
(見下文) 會在遍歷包的 __path__
時被查詢。
包的 __init__.py
文件可以設(shè)置或更改包的 __path__
屬性,而且這是在 PEP 420 之前實現(xiàn)命名空間包的典型方式。 隨著 PEP 420 的引入,命名空間包不再需要提供僅包含 __path__
操控代碼的 __init__.py
文件;導(dǎo)入機制會自動為命名空間包正確地設(shè)置 __path__
。
5.4.6. 模塊的 repr?
默認(rèn)情況下,全部模塊都具有一個可用的 repr,但是你可以依據(jù)上述的屬性設(shè)置,在模塊的規(guī)格說明中更為顯式地控制模塊對象的 repr。
如果模塊具有 spec (__spec__
),導(dǎo)入機制將嘗試用它來生成一個 repr。 如果生成失敗或找不到 spec,導(dǎo)入系統(tǒng)將使用模塊中的各種可用信息來制作一個默認(rèn) repr。 它將嘗試使用 module.__name__
, module.__file__
以及 module.__loader__
作為 repr 的輸入,并將任何丟失的信息賦為默認(rèn)值。
以下是所使用的確切規(guī)則:
如果模塊具有
__spec__
屬性,其中的規(guī)格信息會被用來生成 repr。 被查詢的屬性有 "name", "loader", "origin" 和 "has_location" 等等。如果模塊具有
__file__
屬性,這會被用作模塊 repr 的一部分。如果模塊沒有
__file__
但是有__loader__
且取值不為None
,則加載器的 repr 會被用作模塊 repr 的一部分。對于其他情況,僅在 repr 中使用模塊的
__name__
。
在 3.4 版更改: loader.module_repr()
已棄用,導(dǎo)入機制現(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()
的使用預(yù)定會在 Python 3.12 中停止。
5.4.7. 已緩存字節(jié)碼的失效?
在 Python 從 .pyc
文件加載已緩存字節(jié)碼之前,它會檢查緩存是否由最新的 .py
源文件所生成。 默認(rèn)情況下,Python 通過在所寫入緩存文件中保存源文件的最新修改時間戳和大小來實現(xiàn)這一點。 在運行時,導(dǎo)入系統(tǒng)會通過比對緩存文件中保存的元數(shù)據(jù)和源文件的元數(shù)據(jù)確定該緩存的有效性。
Python 也支持“基于哈希的”緩存文件,即保存源文件內(nèi)容的哈希值而不是其元數(shù)據(jù)。 存在兩種基于哈希的 .pyc
文件:檢查型和非檢查型。 對于檢查型基于哈希的 .pyc
文件,Python 會通過求哈希源文件并將結(jié)果哈希值與緩存文件中的哈希值比對來確定緩存有效性。 如果檢查型基于哈希的緩存文件被確定為失效,Python 會重新生成并寫入一個新的檢查型基于哈希的緩存文件。 對于非檢查型 .pyc
文件,只要其存在 Python 就會直接認(rèn)定緩存文件有效。 確定基于哈希的 .pyc
文件有效性的行為可通過 --check-hash-based-pycs
旗標(biāo)來重載。
在 3.7 版更改: 增加了基于哈希的 .pyc
文件。在此之前,Python 只支持基于時間戳來確定字節(jié)碼緩存的有效性。
5.5. 基于路徑的查找器?
在之前已經(jīng)提及,Python 帶有幾種默認(rèn)的元路徑查找器。 其中之一是 path based finder (PathFinder
),它會搜索包含一個 路徑條目 列表的 import path。 每個路徑條目指定一個用于搜索模塊的位置。
基于路徑的查找器自身并不知道如何進行導(dǎo)入。 它只是遍歷單獨的路徑條目,將它們各自關(guān)聯(lián)到某個知道如何處理特定類型路徑的路徑條目查找器。
默認(rèn)的路徑條目查找器集合實現(xiàn)了在文件系統(tǒng)中查找模塊的所有語義,可處理多種特殊文件類型例如 Python 源碼 (.py
文件),Python 字節(jié)碼 (.pyc
文件) 以及共享庫 (例如 .so
文件)。 在標(biāo)準(zhǔn)庫中 zipimport
模塊的支持下,默認(rèn)路徑條目查找器還能處理所有來自 zip 文件的上述文件類型。
路徑條目不必僅限于文件系統(tǒng)位置。 它們可以指向 URL、數(shù)據(jù)庫查詢或可以用字符串指定的任何其他位置。
基于路徑的查找器還提供了額外的鉤子和協(xié)議以便能擴展和定制可搜索路徑條目的類型。 例如,如果你想要支持網(wǎng)絡(luò) URL 形式的路徑條目,你可以編寫一個實現(xiàn) HTTP 語義在網(wǎng)絡(luò)上查找模塊的鉤子。 這個鉤子(可調(diào)用對象)應(yīng)當(dāng)返回一個支持下述協(xié)議的 path entry finder,以被用來獲取一個專門針對來自網(wǎng)絡(luò)的模塊的加載器。
預(yù)先的警告:本節(jié)和上節(jié)都使用了 查找器 這一術(shù)語,并通過 meta path finder 和 path entry finder 兩個術(shù)語來明確區(qū)分它們。 這兩種類型的查找器非常相似,支持相似的協(xié)議,且在導(dǎo)入過程中以相似的方式運作,但關(guān)鍵的一點是要記住它們是有微妙差異的。 特別地,元路徑查找器作用于導(dǎo)入過程的開始,主要是啟動 sys.meta_path
遍歷。
相比之下,路徑條目查找器在某種意義上說是基于路徑的查找器的實現(xiàn)細節(jié),實際上,如果需要從 sys.meta_path
移除基于路徑的查找器,并不會有任何路徑條目查找器被發(fā)起調(diào)用。
5.5.1. 路徑條目查找器?
path based finder 會負責(zé)查找和加載通過 path entry 字符串來指定位置的 Python 模塊和包。 多數(shù)路徑條目所指定的是文件系統(tǒng)中的位置,但它們并不必受限于此。
作為一種元路徑查找器,path based finder 實現(xiàn)了上文描述的 find_spec()
協(xié)議,但是它還對外公開了一些附加鉤子,可被用來定制模塊如何從 import path 查找和加載。
有三個變量由 path based finder, sys.path
, sys.path_hooks
和 sys.path_importer_cache
所使用。 包對象的 __path__
屬性也會被使用。 它們提供了可用于定制導(dǎo)入機制的額外方式。
sys.path
包含一個提供模塊和包搜索位置的字符串列表。 它初始化自 PYTHONPATH
環(huán)境變量以及多種其他特定安裝和實現(xiàn)的默認(rèn)設(shè)置。 sys.path
條目可指定的名稱有文件系統(tǒng)中的目錄、zip 文件和其他可用于搜索模塊的潛在“位置”(參見 site
模塊),例如 URL 或數(shù)據(jù)庫查詢等。 在 sys.path
中只能出現(xiàn)字符串和字節(jié)串;所有其他數(shù)據(jù)類型都會被忽略。 字節(jié)串條目使用的編碼由單獨的 路徑條目查找器 來確定。
path based finder 是一種 meta path finder,因此導(dǎo)入機制會通過調(diào)用上文描述的基于路徑的查找器的 find_spec()
方法來啟動 import path 搜索。 當(dāng)要向 find_spec()
傳入 path
參數(shù)時,它將是一個可遍歷的字符串列表 —— 通常為用來在其內(nèi)部進行導(dǎo)入的包的 __path__
屬性。 如果 path
參數(shù)為 None
,這表示最高層級的導(dǎo)入,將會使用 sys.path
。
基于路徑的查找器會迭代搜索路徑中的每個條目,并且每次都查找與路徑條目對應(yīng)的 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 相對應(yīng)的 path entry finder。 該異常會被忽略并繼續(xù)進行 import path 的迭代。 每個鉤子應(yīng)該期待接收一個字符串或字節(jié)串對象;字節(jié)串對象的編碼由鉤子決定(例如可以是文件系統(tǒng)使用的編碼 UTF-8 或其它編碼),如果鉤子無法解碼參數(shù),它應(yīng)該引發(fā) ImportError
。
如果 sys.path_hooks
迭代結(jié)束時沒有返回 path entry finder,則基于路徑的查找器 find_spec()
方法將在 sys.path_importer_cache
中存入 None
(表示此路徑條目沒有對應(yīng)的查找器) 并返回 None
,表示此 meta path finder 無法找到該模塊。
如果 sys.path_hooks
中的某個 path entry hook 可調(diào)用對象的返回值 是 一個 path entry finder,則以下協(xié)議會被用來向查找器請求一個模塊的規(guī)格說明,并在加載該模塊時被使用。
當(dāng)前工作目錄 -- 由一個空字符串表示 -- 的處理方式與 sys.path
中的其他條目略有不同。 首先,如果發(fā)現(xiàn)當(dāng)前工作目錄不存在,則 sys.path_importer_cache
中不會存放任何值。 其次,每個模塊查找會對當(dāng)前工作目錄的值進行全新查找。 第三,由 sys.path_importer_cache
所使用并由 importlib.machinery.PathFinder.find_spec()
所返回的路徑將是實際的當(dāng)前工作目錄而非空字符串。
5.5.2. 路徑條目查找器協(xié)議?
為了支持模塊和已初始化包的導(dǎo)入,也為了給命名空間包提供組成部分,路徑條目查找器必須實現(xiàn) find_spec()
方法。
find_spec()
接受兩個參數(shù),即要導(dǎo)入模塊的完整限定名稱,以及(可選的)目標(biāo)模塊。 find_spec()
返回模塊的完全填充好的規(guī)格說明。 這個規(guī)格說明總是包含“加載器”集合(但有一個例外)。
為了向?qū)霗C制提示該規(guī)格說明代表一個命名空間 portion,路徑條目查找器會將 "submodule_search_locations" 設(shè)為一個包含該部分的列表。
在 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ù),即要導(dǎo)入模塊的完整限定名稱。 find_loader()
返回一個 2 元組,其中第一項是加載器而第二項是命名空間 portion。
為了向后兼容其他導(dǎo)入?yún)f(xié)議的實現(xiàn),許多路徑條目查找器也同樣支持元路徑查找器所支持的傳統(tǒng) find_module()
方法。 但是路徑條目查找器 find_module()
方法的調(diào)用絕不會帶有 path
參數(shù)(它們被期望記錄來自對路徑鉤子初始調(diào)用的恰當(dāng)路徑信息)。
路徑條目查找器的 find_module()
方法已棄用,因為它不允許路徑條目查找器為命名空間包提供部分。 如果 find_loader()
和 find_module()
同時存在于一個路徑條目查找器中,導(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)?
替換整個導(dǎo)入系統(tǒng)的最可靠機制是移除 sys.meta_path
的默認(rèn)內(nèi)容,,將其完全替換為自定義的元路徑鉤子。
一個可行的方式是僅改變導(dǎo)入語句的行為而不影響訪問導(dǎo)入系統(tǒng)的其他 API,那么替換內(nèi)置的 __import__()
函數(shù)可能就夠了。 這種技巧也可以在模塊層級上運用,即只在某個模塊內(nèi)部改變導(dǎo)入語句的行為。
想要選擇性地預(yù)先防止在元路徑上從一個鉤子導(dǎo)入某些模塊(而不是完全禁用標(biāo)準(zhǔn)導(dǎo)入系統(tǒng)),只需直接從 find_spec()
引發(fā) ModuleNotFoundError
而非返回 None
就足夠了。 返回后者表示元路徑搜索應(yīng)當(dāng)繼續(xù),而引發(fā)異常則會立即終止搜索。
5.7. 包相對導(dǎo)入?
相對導(dǎo)入使用前綴點號。 一個前綴點號表示相對導(dǎo)入從當(dāng)前包開始。 兩個或更多前綴點號表示對當(dāng)前包的上級包的相對導(dǎo)入,第一個點號之后的每個點號代表一級。 例如,給定以下的包布局結(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
作為可用表達式,但 .moduleY 不是一個有效的表達式。
5.8. 有關(guān) __main__ 的特殊事項?
對于 Python 的導(dǎo)入系統(tǒng)來說 __main__
模塊是一個特殊情況。 正如在 另一節(jié) 中所述,__main__
模塊是在解釋器啟動時直接初始化的,與 sys
和 builtins
很類似。 但是,與那兩者不同,它并不被嚴(yán)格歸類為內(nèi)置模塊。 這是因為 __main__
被初始化的方式依賴于發(fā)起調(diào)用解釋器所附帶的旗標(biāo)和其他選項。
5.8.1. __main__.__spec__?
根據(jù) __main__
被初始化的方式,__main__.__spec__
會被設(shè)置相應(yīng)值或是 None
。
當(dāng) Python 附加 -m
選項啟動時,__spec__
會被設(shè)為相應(yīng)模塊或包的模塊規(guī)格說明。 __spec__
也會在 __main__
模塊作為執(zhí)行某個目錄,zip 文件或其它 sys.path
條目的一部分加載時被填充。
在 其余的情況 下 __main__.__spec__
會被設(shè)為 None
,因為用于填充 __main__
的代碼不直接與可導(dǎo)入的模塊相對應(yīng):
交互型提示
-c
選項從 stdin 運行
直接從源碼或字節(jié)碼文件運行
請注意在最后一種情況中 __main__.__spec__
總是為 None
,即使 文件從技術(shù)上說可以作為一個模塊被導(dǎo)入。 如果想要讓 __main__
中的元數(shù)據(jù)生效,請使用 -m
開關(guān)。
還要注意即使是在 __main__
對應(yīng)于一個可導(dǎo)入模塊且 __main__.__spec__
被相應(yīng)地設(shè)定時,它們?nèi)詴灰暈?不同的 模塊。 這是由于以下事實:使用 if __name__ == "__main__":
檢測來保護的代碼塊僅會在模塊被用來填充 __main__
命名空間時而非普通的導(dǎo)入時被執(zhí)行。
5.9. 開放問題項?
XXX 最好是能增加一個圖表。
XXX * (import_machinery.rst) 是否要專門增加一節(jié)來說明模塊和包的屬性,也許可以擴展或移植數(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. 參考文獻?
導(dǎo)入機制自 Python 誕生之初至今已發(fā)生了很大的變化。 原始的 包規(guī)格說明 仍然可以查閱,但在撰寫該文檔之后許多相關(guān)細節(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__
屬性,用于在模塊中的顯式相對導(dǎo)入。
PEP 328 引入了絕對和顯式相對導(dǎo)入,并初次提出了 __name__
語義,最終由 PEP 366 為 __package__
加入規(guī)范描述。
PEP 338 定義了將模塊作為腳本執(zhí)行。
PEP 451 在 spec 對象中增加了對每個模塊導(dǎo)入狀態(tài)的封裝。 它還將加載器的大部分樣板責(zé)任移交回導(dǎo)入機制中。 這些改變允許棄用導(dǎo)入系統(tǒng)中的一些 API 并為查找器和加載器增加一些新的方法。
備注
- 1
參見
types.ModuleType
。- 2
importlib 實現(xiàn)避免直接使用返回值。 而是通過在
sys.modules
中查找模塊名稱來獲取模塊對象。 這種方式的間接影響是被導(dǎo)入的模塊可能在sys.modules
中替換其自身。 這屬于具體實現(xiàn)的特定行為,不保證能在其他 Python 實現(xiàn)中起作用。- 3
在遺留代碼中,有可能在
sys.path_importer_cache
中找到imp.NullImporter
的實例。 建議將這些代碼修改為使用None
代替。 詳情參見 Porting Python code。