PEP 451 – 匯入系統的 ModuleSpec 型別
- 作者:
- Eric Snow <ericsnowcurrently at gmail.com>
- BDFL 委託:
- Brett Cannon <brett at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
- 討論至:
- Import-SIG 郵件列表
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 建立日期:
- 2013年8月8日
- Python 版本:
- 3.4
- 釋出歷史:
- 2013年8月8日,2013年8月28日,2013年9月18日,2013年9月24日,2013年10月4日
- 決議:
- Python-Dev 訊息
摘要
本 PEP 提議在 importlib.machinery 中新增一個名為“ModuleSpec”的新類。它將提供用於載入模組的所有與匯入相關的資訊,並且無需先載入模組即可使用。查詢器將直接提供模組的規範,而不是載入器(它們將繼續間接提供載入器)。匯入機制將進行調整以利用模組規範,包括使用它們來載入模組。
術語和概念
此提案中的更改是使一些現有術語和概念更加清晰的機會,而目前它們(不幸地)是模稜兩可的。此提案還引入了新概念。最後,值得解釋一些其他人可能不熟悉的其他現有術語。為了方便理解,這裡簡要概述了所有三組術語和概念。有關匯入系統的更詳細解釋,請參閱 [2]。
名稱
在此提案中,模組的“名稱”指的是其完全限定名,這意味著模組父級(如果有)的完全限定名與模組的簡單名稱透過句點連線起來。
查詢器
“查詢器”是一個物件,它識別匯入系統應該用於載入模組的載入器。目前這是透過呼叫查詢器的 find_module() 方法來完成的,該方法返回載入器。
查詢器嚴格負責提供載入器,它們透過其 find_module() 方法實現。然後匯入系統使用該載入器來載入模組。
載入器
“載入器”是一個在匯入期間用於載入模組的物件。目前這是透過呼叫載入器的 load_module() 方法來完成的。載入器還可以提供 API,用於獲取它能載入的模組以及與此類模組相關的源資料的資訊。
目前,載入器(透過 load_module())負責某些樣板的、與匯入相關的操作。這些操作是:
- 執行一些(與模組相關的)驗證
- 建立模組物件
- 在模組上設定與匯入相關的屬性
- 將模組“註冊”到 sys.modules
- 執行模組
- 在載入模組失敗時進行清理
所有這些都在匯入系統呼叫 Loader.load_module() 期間發生。
來源
這是一個新術語和概念。它的思想已經巧妙地存在於匯入系統中,但本提案明確了這一概念。
匯入上下文中的“來源”是指模組來源的系統(或系統內的資源)。就本提案而言,“來源”也是一個字串,用於標識此類資源或系統。“來源”適用於所有模組。
例如,內建模組和凍結模組的來源是直譯器本身。匯入系統已經將此來源分別標識為“built-in”和“frozen”。這在以下模組 repr 中得到體現:“<module 'sys' (built-in)>”。
事實上,模組 repr 已經是一個相對可靠但隱含的模組來源指示器。其他模組也透過其他方式指示其來源,如“位置”條目中描述的那樣。
由載入器決定如何解釋和使用模組的來源(如果使用的話)。
位置
這是一個新術語。然而,這個概念已經明確存在於匯入系統中,與模組的 __file__ 和 __path__ 屬性以及其他地方的名稱/術語“path”相關聯。
“位置”是一種資源或“地方”,而不是一個大系統,模組從此處載入。它屬於“來源”的範疇。位置的例子包括檔案系統路徑和 URL。位置由資源的名稱標識,但不一定標識資源所屬的系統。在這種情況下,載入器必須自行標識系統。
與其他型別的模組來源不同,載入器不能僅憑模組名稱推斷出位置。相反,必須向載入器提供一個字串來標識位置,通常由生成載入器的查詢器提供。然後,載入器使用此資訊來定位將從中載入模組的資源。理論上,您可以在給定位置下以各種名稱載入模組。
匯入系統中最常見的位置示例是載入源模組和擴充套件模組的檔案。對於這些模組,位置由 __file__ 屬性中的字串標識。儘管 __file__ 對於某些模組(例如壓縮模組)來說並不特別準確,但它目前是匯入系統指示模組具有位置的唯一方式。
具有位置的模組可以稱為“可定位的”。
快取
匯入系統將編譯後的模組儲存在 `__pycache__` 目錄中作為最佳化。我們今天使用的這個模組快取是由 PEP 3147 提供的。對於本提案,模組快取的相關 API 是模組的 __cache__ 屬性和 importlib.util 中的 `cache_from_source()` 函式。載入器負責將模組放入快取(並從快取中載入)。目前,快取僅用於編譯後的源模組。然而,載入器可以利用模組快取來處理其他型別的模組。
包
概念不變,術語也不變。然而,模組和包之間的區別大多是表面上的。包*是*模組。它們只是有一個 __path__ 屬性,並且匯入可能會新增繫結到子模組的屬性。通常感知的差異是混淆的來源。本提案明確地弱化了在有意義的情況下包和模組之間的區別。
動機
匯入系統在 Python 的生命週期中不斷發展。2002年末,PEP 302 透過查詢器、載入器和 sys.meta_path 引入了標準化的匯入鉤子。Python 3.1 引入的 importlib 模組現在公開了 PEP 302 描述的 API 以及整個匯入系統的純 Python 實現。現在理解和擴充套件匯入系統變得更加容易。雖然這對 Python 社群有益,但這種更高的可訪問性也帶來了挑戰。
隨著越來越多的開發人員理解和定製匯入系統,查詢器和載入器 API 中的任何弱點都將產生更大的影響。因此,我們越早解決匯入系統中的任何此類弱點越好……本提案希望解決其中的兩個。
首先,每當匯入系統需要儲存有關模組的資訊時,我們最終會在模組物件上新增更多屬性,這些屬性通常只對匯入系統有意義。最好有一個每模組名稱空間,用於放置未來與匯入相關的資訊並在匯入系統內部傳遞。其次,查詢器和載入器之間存在 API 空白,導致遇到時出現不必要的複雜性。PEP 420(名稱空間包)實現不得不為此進行變通。這種複雜性在最近關於另一個提案的努力中再次浮現。[1]
上面 查詢器 和 載入器 部分詳細說明了兩者的當前職責。值得注意的是,載入器無需透過其他方法提供其 load_module() 方法的任何功能。因此,儘管有關模組的匯入相關資訊可能無需載入模組即可獲得,但它並未以其他方式公開。
此外,與 load_module() 相關的要求對所有載入器都是通用的,並且大多以完全相同的方式實現。這意味著每個載入器都必須複製相同的樣板程式碼。importlib.util 提供了一些有助於解決此問題的工具,但如果匯入系統簡單地承擔這些職責,則會更有幫助。問題是這將限制 load_module() 易於繼續促進的定製程度。
更重要的是,雖然查詢器*可以*提供載入器的 load_module() 所需的資訊,但它目前沒有一致的方法將其傳遞給載入器。這是本提案旨在彌補的查詢器和載入器之間的空白。
最後,當匯入系統呼叫查詢器的 find_module() 時,查詢器會使用各種關於模組的資訊,這些資訊在方法上下文之外也很有用。目前,在方法呼叫之後保留該每模組資訊的選項有限,因為它只返回載入器。解決此限制的流行選項是將資訊儲存在查詢器本身的模組到資訊對映中,或將其儲存在載入器上。
不幸的是,載入器並非必需針對特定模組。最重要的是,查詢器可以提供的一些有用資訊對所有查詢器都是通用的,因此理想情況下,匯入系統可以處理這些細節。這與之前查詢器和載入器之間的空白相同。
作為這個缺陷導致複雜性的一個例子,Python 3.3 中名稱空間包的實現(參見 PEP 420)添加了 FileFinder.find_loader(),因為 find_module() 沒有好的方法來提供名稱空間搜尋位置。
解決這個空白的方法是使用 ModuleSpec 物件,它包含每模組資訊並處理涉及載入模組的樣板功能。
規範
目標是彌合查詢器和載入器之間的差距,同時儘可能少地改變它們的語義。儘管一些功能和資訊被轉移到新的 ModuleSpec 型別,但它們的行為應保持不變。然而,為了清晰起見,查詢器和載入器語義將明確標識。
以下是本 PEP 描述的更改的高階摘要。更多詳細資訊可在後續部分中找到。
importlib.machinery.ModuleSpec (新增)
在匯入過程中,模組匯入系統相關狀態的封裝。有關更詳細的描述,請參見下面的 ModuleSpec 部分。
- ModuleSpec(name, loader, *, origin=None, loader_state=None, is_package=None)
屬性
- name - 模組完全限定名的字串。
- loader - 用於載入的載入器。
- origin - 模組載入來源的名稱,例如內建模組的“builtin”和從源載入模組的檔名。
- submodule_search_locations - 如果是包,則為查詢子模組的字串列表(否則為 None)。
- loader_state - 一個包含額外模組特定資料的容器,用於載入期間使用。
- cached (屬性) - 編譯模組應該儲存位置的字串。
- parent (只讀屬性) - 模組作為子模組所屬包的完全限定名(或 None)。
- has_location (只讀屬性) - 一個標誌,指示模組的“origin”屬性是否引用了位置。
importlib.util 附加功能
這些是 ModuleSpec 工廠函式,旨在方便查詢器。有關更多詳細資訊,請參見下面的 工廠函式 部分。
- spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None) - 從檔案資訊和載入器 API 構建規範。
- spec_from_loader(name, loader, *, origin=None, is_package=None) - 使用載入器 API 填充缺失資訊來構建規範。
其他 API 附加功能
- importlib.find_spec(name, path=None, target=None) 將與 importlib.find_loader()(它取代了該方法)完全相同,但返回一個規範而不是載入器。
對於查詢器
- importlib.abc.MetaPathFinder.find_spec(name, path, target) 和 importlib.abc.PathEntryFinder.find_spec(name, target) 將返回一個模組規範,用於匯入期間。
對於載入器
- importlib.abc.Loader.exec_module(module) 將在自己的名稱空間中執行模組。它取代了 importlib.abc.Loader.load_module(),接管了其模組執行功能。
- importlib.abc.Loader.create_module(spec)(可選)將返回用於載入的模組。
對於模組
- 模組物件將有一個新屬性:
__spec__。
API 更改
- InspectLoader.is_package() 將變為可選。
棄用
- importlib.abc.MetaPathFinder.find_module()
- importlib.abc.PathEntryFinder.find_module()
- importlib.abc.PathEntryFinder.find_loader()
- importlib.abc.Loader.load_module()
- importlib.abc.Loader.module_repr()
- importlib.util.set_package()
- importlib.util.set_loader()
- importlib.find_loader()
刪除項
這些是在 Python 3.4 釋出之前引入的,因此它們可以簡單地刪除。
- importlib.abc.Loader.init_module_attrs()
- importlib.util.module_to_load()
其他更改
- importlib 中匯入系統的實現將更改為使用 ModuleSpec。
- importlib.reload() 將使用 ModuleSpec。
- 模組的匯入相關屬性(除了
__spec__)在該模組匯入期間將不再被匯入系統直接使用。然而,這不影響在載入其他模組(例如子模組)時使用這些屬性(例如__path__)。 - 除了匯入系統之外,不應再直接向模組新增與匯入相關的屬性。
- 模組型別的
__repr__()將是一個圍繞純 Python 實現的薄包裝器,它將利用 ModuleSpec。 __main__模組的規範將反映適當的名稱和來源。
向後相容性
- 如果查詢器沒有定義 find_spec(),則會從 find_module() 返回的載入器派生出一個規範。
- PathEntryFinder.find_loader() 仍然優先於 find_module()。
- 如果 exec_module() 未定義,則使用 Loader.load_module()。
什麼不會改變?
- import 語句的語法和語義。
- 現有查詢器和載入器將繼續正常工作。
- 與匯入相關的模組屬性仍將使用相同的資訊進行初始化。
- 查詢器仍將建立載入器(現在將其儲存在規範中)。
- Loader.load_module(),如果模組定義了它,將具有所有相同的要求,並且仍然可以直接呼叫。
- 載入器仍將負責模組資料 API。
- importlib.reload() 仍將覆蓋與匯入相關的屬性。
職責
以下是此 PEP 之後職責的快速分解。
查詢器
- 建立/識別可以載入模組的載入器。
- 為模組建立規範。
載入器
- 建立模組(可選)。
- 執行模組。
ModuleSpec
- 協調模組載入
- 模組載入的樣板,包括管理 sys.modules 和設定與匯入相關的屬性
- 如果載入器沒有建立模組,則建立模組
- 呼叫 loader.exec_module(),傳入要執行的模組
- 包含載入器執行模組所需的所有資訊
- 提供模組的 repr
現有的查詢器和載入器需要做些什麼改變?
立即?什麼都不需要。現狀將被廢棄,但將繼續工作。然而,以下是查詢器和載入器的作者應根據此 PEP 進行更改的事項:
- 在查詢器上實現 find_spec()。
- 如果可能,在載入器上實現 exec_module()。
importlib.util 中的 ModuleSpec 工廠函式旨在幫助轉換現有查詢器。在此方面,spec_from_loader() 和 spec_from_file_location() 都是直接的實用程式。
對於現有載入器,exec_module() 應該是一個相對直接的轉換,從 load_module() 的非樣板部分進行。在某些不常見的情況下,載入器也應該實現 create_module()。
ModuleSpec 使用者
ModuleSpec 物件有 3 個不同的目標受眾:Python 本身、匯入鉤子和普通 Python 使用者。
Python 將在匯入機制、直譯器啟動和各種標準庫模組中使用規範。有些模組是面向匯入的,如 pkgutil,而另一些則不是,如 pickle 和 pydoc。在所有情況下,都將使用完整的 ModuleSpec API。
匯入鉤子(查詢器和載入器)將以特定方式使用規範。首先,查詢器可以使用 importlib.util 中的規範工廠函式來建立規範物件。它們還可以在建立規範後直接調整規範屬性。其次,查詢器可以將額外資訊繫結到規範(在 finder_extras 中),供載入器在模組建立/執行期間使用。最後,載入器在建立和/或執行模組時將使用規範上的屬性。
Python 使用者將能夠檢查模組的 __spec__ 以獲取有關該物件的匯入相關資訊。通常,Python 應用程式和互動式使用者將不使用 ModuleSpec 工廠函式或任何例項方法。
載入如何工作
以下是匯入機制在載入期間所做工作的概述,已調整以利用模組的規範和新的載入器 API:
module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
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 and spec.submodule_search_locations is not None:
# Namespace package
sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
spec.loader.load_module(spec.name)
# __loader__ and __package__ would be explicitly set here for
# backwards-compatibility.
else:
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
except BaseException:
try:
del sys.modules[spec.name]
except KeyError:
pass
raise
module_to_return = sys.modules[spec.name]
這些步驟正是 Loader.load_module() 已經期望執行的操作。因此,載入器將被簡化,因為它們只需要實現 exec_module()。
請注意,我們必須從 sys.modules 返回模組。在載入期間,模組可能已經在 sys.modules 中替換了自身。由於我們沒有後匯入鉤子 API 來適應這種用例,我們必須處理它。然而,在替換情況下,我們不必擔心在物件上設定與匯入相關的模組屬性。如果模組編寫者這樣做,他們需要自行處理。
重新載入如何工作
這是 reload() 的相應概述
_RELOADING = {}
def reload(module):
try:
name = module.__spec__.name
except AttributeError:
name = module.__name__
spec = find_spec(name, target=module)
if sys.modules.get(name) is not module:
raise ImportError
if spec in _RELOADING:
return _RELOADING[name]
_RELOADING[name] = module
try:
if spec.loader is None:
# Namespace loader
_init_module_attrs(spec, module)
return module
if spec.parent and spec.parent not in sys.modules:
raise ImportError
_init_module_attrs(spec, module)
# Ignoring backwards-compatibility call to load_module()
# for simplicity.
spec.loader.exec_module(module)
return sys.modules[name]
finally:
del _RELOADING[name]
這裡的一個關鍵點是,切換到 Loader.exec_module() 意味著載入器在執行時將不再容易知道是否是重新載入。在此提案之前,它們只需檢查模組是否已在 sys.modules 中。現在,當 exec_module() 在載入(而非重新載入)期間被呼叫時,匯入機制已經將模組放置在 sys.modules 中。這是 find_spec() 具有 “target”引數 的部分原因。
ModuleSpec
屬性
以下每個名稱都是 ModuleSpec 物件上的一個屬性。None 值表示“未設定”。這與模組物件不同,後者中該屬性根本不存在。大多數屬性對應於模組的匯入相關屬性。這是對映。此對映的反向描述了匯入機制在呼叫 exec_module() 之前如何設定模組屬性。
| 在 ModuleSpec 上 | 在模組上 |
|---|---|
| 名稱 | __name__ |
| 載入器 | __loader__ |
| parent | __package__ |
| 來源 | __file__* |
| cached | __cached__*,** |
| submodule_search_locations | __path__** |
| loader_state | - |
| has_location | - |
雖然 parent 和 has_location 是隻讀屬性,但其餘屬性可以在模組規範建立後甚至在匯入完成後替換。這允許在直接修改規範是最佳選擇的異常情況下進行修改。然而,典型用法不應涉及更改模組規範的狀態。
來源
“origin”是一個字串,表示模組的來源。參見上面的origin。除了資訊價值外,它還在模組的 repr 中使用。在“has_location”為真的規範情況下,__file__ 被設定為“origin”的值。對於內建模組,“origin”將設定為“built-in”。
has_location
如上述 位置 部分所述,許多模組都是“可定位的”,這意味著存在一個相應的資源,模組將從該資源載入,並且該資源可以用字串描述。相反,不可定位的模組無法以這種方式載入,例如內建模組和在程式碼中動態建立的模組。對於這些模組,名稱是訪問它們的唯一方式,因此它們具有“origin”但沒有“location”。
如果模組是可定位的,則“has_location”為 true。在這種情況下,規範的 origin 用作位置,__file__ 被設定為 spec.origin。如果需要額外的位置資訊(例如 zipimport),該資訊可以儲存在 spec.loader_state 中。
“has_location”可以從載入器上是否存在 load_data() 方法推斷出來。
順便說一下,並非所有可定位模組都是可快取的,但大多數都是。
submodule_search_locations
位置字串列表,通常是目錄路徑,用於搜尋子模組。如果模組是一個包,則它將被設定為一個列表(即使是空列表)。否則為 None。
對應的模組屬性名稱 __path__ 相對模糊。我們不將其映象,而是使用更明確的屬性名稱來明確其目的。
loader_state
查詢器可以將 loader_state 設定為任何值,以提供額外資料供載入器在載入期間使用。None 值是預設值,表示沒有額外資料。否則,它可以設定為任何物件,例如 dict、list 或 types.SimpleNamespace,包含相關額外資訊。
例如,zipimporter 可以用它直接將 zip 歸檔名稱傳遞給載入器,而無需從 origin 派生或為每個查詢操作建立自定義載入器。
loader_state 旨在供查詢器和相應載入器使用。它不保證是任何其他用途的穩定資源。
工廠函式
spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None)
從檔案資訊和載入器 API 構建規範。
- “origin”將設定為位置。
- “has_location”將設定為 True。
- “cached”將設定為呼叫 cache_from_source() 的結果。
- 如果未傳入“location”,則“origin”可以從 loader.get_filename() 推斷。
- 如果 location 是檔名,則“loader”可以從字尾推斷。
- 如果 location 是檔名,則“submodule_search_locations”可以從 loader.is_package() 和 os.path.dirname(location) 推斷。
spec_from_loader(name, loader, *, origin=None, is_package=None)
透過使用載入器 API 填充缺失資訊來構建規範。
- “has_location”可以從 loader.get_data 推斷。
- “origin”可以從 loader.get_filename() 推斷。
- 如果 location 是檔名,則“submodule_search_locations”可以從 loader.is_package() 和 os.path.dirname(location) 推斷。
向後相容性
ModuleSpec 沒有任何。如果 Finder.find_module() 返回模組規範而不是載入器,那將是另一回事。在這種情況下,規範必須像原本會返回的載入器一樣工作。這樣做相對簡單,但會造成不必要的複雜性。它是本 PEP 早期版本的一部分。
子類化
ModuleSpec 的子類是允許的,但應該沒有必要。簡單地設定 loader_state 或向自定義查詢器或載入器新增功能可能更合適,應該首先嚐試。然而,只要子類仍然滿足匯入系統的要求,該型別的物件作為 Finder.find_spec() 的返回值是完全可以的。同樣的觀點也適用於鴨子型別。
現有型別
模組物件
除了新增 __spec__ 之外,所有其他與匯入相關的模組屬性都不會被更改或廢棄,儘管其中一些可能會;任何此類廢棄都可以等到 Python 4。
模組的規範不會與相應的匯入相關屬性保持同步。儘管它們可能有所不同,但在實踐中它們通常是相同的。
一個值得注意的例外是,當模組使用 -m 標誌作為指令碼執行時。在這種情況下,module.__spec__.name 將反映實際的模組名稱,而 module.__name__ 仍將是 __main__。
兩個同名模組的規範不保證相同。同樣,不保證連續呼叫 importlib.find_spec() 會返回相同的物件或等效物件,儘管後者至少很可能發生。
查詢器
查詢器仍然負責識別並通常建立應載入模組的載入器。該載入器現在將儲存在 find_spec() 返回的模組規範中,而不是直接返回。與本 PEP 之前的情況一樣,如果載入器建立成本很高,則可以將其設計為將成本推遲到以後。
MetaPathFinder.find_spec(name, path=None, target=None)
PathEntryFinder.find_spec(name, target=None)
當呼叫 find_spec() 時,查詢器必須返回 ModuleSpec 物件。這個新方法取代了 find_module() 和 find_loader()(在 PathEntryFinder 的情況下)。如果載入器沒有 find_spec(),則為了向後相容性,會轉而使用 find_module() 和 find_loader()。
在載入器中新增另一個類似的方法是出於實用性的考慮。find_module() 可以更改為返回規範而不是載入器。這很誘人,因為匯入 API 已經受夠了,尤其是考慮到 PathEntryFinder.find_loader() 剛剛在 Python 3.3 中新增。然而,額外的複雜性和一個不那麼明確的方法名稱是不值得的。
find_spec() 的“target”引數
對 find_spec() 的呼叫可以選擇包含“target”引數。這是隨後將用作載入目標的模組物件。在正常匯入(和預設情況下)“target”為 None,這意味著目標模組尚未建立。在重新載入期間,傳遞給 reload() 的模組將作為 target 傳遞給 find_spec()。此引數允許查詢器構建模組規範,其中包含比其他可用資訊更多的資訊。這樣做在識別要使用的載入器方面尤其重要。
透過 find_spec(),查詢器將始終識別它將在規範中返回的載入器(或返回 None)。在識別載入器時,查詢器還應決定載入器是否支援載入到目標模組中,以防傳入“target”。此決定可能需要與載入器協商。
如果查詢器確定載入器不支援載入到目標模組中,它應該要麼尋找另一個載入器,要麼引發 ImportError(完全停止模組的匯入)。這種確定在重新載入期間尤為重要,因為如 重新載入如何工作 中所述,載入器將無法再自行輕易識別重新載入情況。
“target”引數提出了兩種替代方案:Loader.supports_reload() 和將“target”新增到 Loader.exec_module() 而不是 find_spec()。supports_reload() 是最初解決重新載入情況的方法。[6] 然而,對這種特定於載入器、以重新載入為中心的方法存在一些反對意見。[7]
至於 exec_module() 上的“target”,載入器在重新載入期間可能需要目標模組(或規範)中的其他資訊,而不僅僅是“此載入器是否支援重新載入此模組”,這在脫離 load_module() 後不再可用。一個擺在桌面上的提議是在 exec_module() 中新增類似“target”的東西。[8] 然而,將“target”放在 find_spec() 上更符合本 PEP 的目標。此外,它消除了對 supports_reload() 的需求。
名稱空間包
目前,路徑入口查詢器可以從 find_loader() 返回 (None, portions) 來指示它找到了可能的名稱空間包的一部分。為了實現相同的效果,find_spec() 必須返回一個規範,其中“loader”設定為 None(也即未設定),並且 submodule_search_locations 設定為與 find_loader() 提供相同的 portions。PathFinder 如何處理此類規範取決於它。
載入器
Loader.exec_module(module)
載入器將有一個新方法 exec_module()。它的唯一工作是“執行”模組,從而填充模組的名稱空間。它不負責建立或準備模組物件,也不負責後續的任何清理。它沒有返回值。exec_module() 將在載入和重新載入期間使用。
exec_module() 應該正確處理多次呼叫的情況。對於某些型別的模組,這可能意味著在方法第一次呼叫之後,每次都引發 ImportError。這與重新載入特別相關,因為某些型別的模組不支援就地重新載入。
Loader.create_module(spec)
載入器也可以實現 create_module(),它將返回一個要執行的新模組。它可以返回 None 來表示應該使用預設的模組建立程式碼。create_module() 的一個用例(儘管不典型)是提供一個作為內建模組型別子類的模組。大多數載入器不需要實現 create_module()。
create_module() 應該正確處理同一個 spec/module 多次呼叫的情況。這可能包括返回 None 或引發 ImportError。
注意
exec_module() 和 create_module() 不應設定任何與匯入相關的模組屬性。load_module() 這樣做是一個設計缺陷,本提案旨在糾正它。
其他更改
PEP 420 引入了可選的 module_repr() 載入器方法,以限制模組型別 __repr__() 中的特殊處理量。由於此方法是 ModuleSpec 的一部分,它將在載入器上被廢棄。然而,如果它存在於載入器上,它將獨佔使用。
Loader.init_module_attr() 方法,在 Python 3.4 釋出之前新增,將為了 ModuleSpec 上的相同方法而被刪除。
然而,InspectLoader.is_package() 不會被廢棄,儘管相同的資訊在 ModuleSpec 上也能找到。如果該資訊無法從其他地方獲得,ModuleSpec 可以使用它來填充自己的 is_package。不過,它將變為可選。
除了在載入期間執行模組之外,載入器仍將直接負責提供有關模組相關資料的 API。
其他更改
- importlib 提供的各種查詢器和載入器將更新以符合本提案。
- stdlib 中任何其他匯入相關 API(特別是查詢器和載入器)的實現或依賴也將相應地調整以符合此 PEP。雖然它們應該繼續工作,但任何遺漏的此類更改都應被視為 Python 3.4.x 系列的錯誤。
__main__模組的規範將反映直譯器啟動的方式。例如,使用-m時,規範的名稱將是所用模組的名稱,而__main__.__name__仍將是“__main__”。- 我們將新增 importlib.find_spec() 以映象 importlib.find_loader()(該方法將被廢棄)。
- importlib.reload() 更改為使用 ModuleSpec。
- importlib.reload() 現在將使用每模組匯入鎖。
參考實現
參考實現可在 http://bugs.python.org/issue18864 獲得。
實現說明
* 本 PEP 的實現需要注意其對 pkgutil(和 setuptools)的影響。pkgutil 有一些基於通用函式的 PEP 302 擴充套件,如果 importlib 在工具不知情的情況下開始包裝載入器,可能會導致問題。
* 其他要檢視的模組:runpy(和 pythonrun.c)、pickle、pydoc、inspect。
例如,在 __main__ 情況下,pickle 應該更新以檢視 module.__spec__.name。
被拒絕的 PEP 補充
有一些提議的補充內容與本提案的範圍不符。
沒有“PathModuleSpec”ModuleSpec 子類來分離 has_location、cached 和 submodule_search_locations。雖然這可能會使分離更清晰,但模組物件沒有這種區別。ModuleSpec 將同樣良好地支援這兩種情況。
雖然“ModuleSpec.is_package”將是一個簡單的附加屬性(別名為 self.submodule_search_locations is not None),但它延續了模組和包之間人為的(且大部分是錯誤的)區別。
模組規範 工廠函式 可以是 ModuleSpec 上的類方法。然而,這會透過 __spec__ 將它們暴露給*所有*模組,這可能會不必要地混淆非高階 Python 使用者。工廠函式有一個特定的用例,即支援查詢器作者。請參閱 ModuleSpec 使用者。
同樣,ModuleSpec 還可以新增其他幾個方法,這些方法暴露了匯入機制對模組規範的特定使用:
- create() - Loader.create_module() 的包裝器。
- exec(module) - Loader.exec_module() 的包裝器。
- load() - 已廢棄的 Loader.load_module() 的類似物。
與工廠函式一樣,透過 module.__spec__ 暴露這些方法並不理想。它們最終會成為一個有吸引力的麻煩,即使只作為“私有”屬性暴露(正如它們在本文件的早期版本中所做的那樣)。如果有人後來發現需要這些方法,我們可以在那時透過適當的 API(獨立於 ModuleSpec)暴露它們,也許與 PEP 406(匯入引擎)相關。
可以想象,load() 方法可以選擇接受一個模組列表進行互動,而不是 sys.modules。此外,load() 可以用於實現多版本匯入。這兩個都是有趣的想法,但肯定超出了本提案的範圍。
其他遺漏
- 新增 ModuleSpec.submodules (RO-property) - 返回相對於規範的可能子模組。
- 新增 ModuleSpec.loaded (RO-property) - sys.module 中載入的模組(如果有)。
- 新增 ModuleSpec.data - 一個描述符,包裝規範載入器的資料 API。
- 另請參閱 [3]。
參考資料
版權
本文件已置於公共領域。
來源:https://github.com/python/peps/blob/main/peps/pep-0451.rst
上次修改時間:2025-02-01 08:59:27 GMT