PEP 547 – 使用 -m 選項執行擴充套件模組
- 作者:
- Marcel Plch <gmarcel.plch at gmail.com>,Petr Viktorin <encukou at gmail.com>
- 狀態:
- 推遲
- 型別:
- 標準跟蹤
- 建立日期:
- 2017年5月25日
- Python 版本:
- 3.7
- 釋出歷史:
推遲通知
Cython——此 PEP 最重要的用例,也是唯一明確的用例——尚未為多階段初始化做好準備。它在 C 級靜態變數中保留全域性狀態。請參閱 Cython issue 1923 中的討論。
在情況改變之前,此 PEP 將被推遲。
摘要
此 PEP 提出了允許內建模組和擴充套件模組使用 PEP 489 多階段初始化在 __main__ 名稱空間中執行的實現。
透過此功能,啟用多階段初始化的模組可以使用以下命令執行
$ python3 -m _testmultiphase
This is a test module named __main__.
動機
目前,擴充套件模組不支援 Python 源模組的所有功能。具體來說,無法使用 Python 的 -m 選項將擴充套件模組作為指令碼執行。
實現此目的的技術基礎已為 PEP 489 完成,並且在那個 PEP 的“未來可能擴充套件”部分列出了啟用 -m 選項。從技術上講,此處提出的額外更改相對較小。
基本原理
擴充套件模組缺乏對 -m 選項的支援,傳統上透過提供一個 Python 包裝器來解決。例如,_pickle 模組的命令列介面在純 Python pickle 模組中(以及純 Python 的重新實現)。
這對於標準庫模組來說效果很好,因為使用 C API 構建命令列介面很麻煩。然而,其他使用者可能希望直接建立可執行的擴充套件模組。
一個重要的用例是 Cython,一種類似 Python 的語言,它編譯成 C 擴充套件模組。Cython 是 Python 的一個(近乎)超集,這意味著用 Cython 編譯 Python 模組通常不會改變模組的功能,允許逐步新增 Cython 特有的功能。這個 PEP 將允許 Cython 擴充套件模組在使用 -m 選項執行時,其行為與對應的 Python 模組相同。Cython 開發者認為此功能值得實現(參見 Cython issue 1715)。
背景
Python 的 -m 選項由函式 runpy._run_module_as_main 處理。
由 -m 指定的模組不會正常匯入。相反,它在 __main__ 模組的名稱空間中執行,該模組在直譯器初始化早期建立。
對於 Python 源模組,在另一個模組的名稱空間中執行不是問題:程式碼執行時 locals 和 globals 都設定為現有模組的 __dict__。對於擴充套件模組則不然,其 PyInit_* 入口點傳統上既建立了一個新的模組物件(使用 PyModule_Create),又對其進行了初始化。
自 Python 3.5 起,擴充套件模組可以使用 PEP 489 多階段初始化。在此場景下,PyInit_* 入口點返回一個 PyModuleDef 結構體:描述模組應如何建立和初始化的資訊。擴充套件可以選擇使用 Py_mod_create 回撥來定製模組物件的建立,或者透過不指定 Py_mod_create 來選擇使用普通模組物件。另一個回撥 Py_mod_exec 隨後被呼叫以初始化模組物件,例如透過用方法和類填充它。
提案
多階段初始化使得在另一個模組的名稱空間中執行擴充套件模組成為可能:如果未指定 Py_mod_create 回撥,則可以將 __main__ 模組傳遞給 Py_mod_exec 回撥進行初始化,就好像 __main__ 是一個全新構建的模組物件一樣。
此方案中的一個複雜之處是 C 級模組狀態。每個模組都有一個 md_state 指標,指向建立擴充套件模組時分配的記憶體區域。PyModuleDef 指定了要分配多少記憶體。
實現必須確保 md_state 記憶體最多隻分配一次。此外,Py_mod_exec 回撥應該每個模組只調用一次。多次初始化模組的影響過於微妙,不應期望擴充套件作者對其進行推斷。md_state 指標本身將作為防護:分配記憶體和呼叫 Py_mod_exec 將始終同時進行,如果 md_state 已經非 NULL,則初始化擴充套件模組將失敗。
由於 __main__ 模組不是作為擴充套件模組建立的,因此其 md_state 通常為 NULL。在 __main__ 的上下文中初始化擴充套件模組之前,其模組狀態將根據該模組的 PyModuleDef 進行分配。
雖然 PEP 489 旨在使這些更改普遍可行,但有必要將擴充套件模組的模組發現、建立和初始化步驟解耦,以便可以使用另一個模組而不是新初始化的模組,並且需要將此功能新增到 runpy 和 importlib 中。
規範
將為 importlib 載入器新增一個可選的新方法。此方法將命名為 exec_in_module,並接受兩個位置引數:模組規範和一個已存在的模組。模組上已設定的任何匯入相關屬性,例如 __spec__ 或 __name__,將被忽略。
runpy._run_module_as_main 函式將尋找這個新的載入器方法。如果它存在,runpy 將執行它,而不是嘗試載入和執行模組的 Python 程式碼。否則,runpy 將像以前一樣操作。
ExtensionFileLoader 變更
importlib 的 ExtensionFileLoader 將獲得 exec_in_module 的實現,該實現將呼叫一個新函式 _imp.exec_in_module。
_imp.exec_in_module 將使用現有機制查詢並呼叫擴充套件模組的 PyInit_* 函式。
PyInit_* 函式可以返回一個完全初始化的模組(單階段初始化)或一個 PyModuleDef(用於 PEP 489 多階段初始化)。
在單階段初始化情況下,_imp.exec_in_module 將引發 ImportError。
在多階段初始化情況下,PyModuleDef 和待初始化的模組將被傳遞給一個新的函式 PyModule_ExecInModule。
如果 PyModuleDef 指定了 Py_mod_create 槽位,或者模組已經初始化(即其 md_state 指標不是 NULL),此函式將引發 ImportError。否則,該函式將根據 PyModuleDef 初始化模組。
向後相容性
此 PEP 保持向後相容性。它只添加了新函式,以及為之前不支援將模組作為 __main__ 執行的載入器添加了一個新的載入器方法。
參考實現
此 PEP 的參考實現可在 GitHub 上找到。
版權
本文件已置於公共領域。
來源:https://github.com/python/peps/blob/main/peps/pep-0547.rst