Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 489 – 多階段擴充模組初始化

作者:
Petr Viktorin <encukou at gmail.com>, Stefan Behnel <stefan_ml at behnel.de>, Alyssa Coghlan <ncoghlan at gmail.com>
BDFL-Delegate:
Eric Snow <ericsnowcurrently at gmail.com>
討論於:
Import-SIG 郵件列表
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
建立日期:
2013年8月11日
Python 版本:
3.5
公告歷史:
2013年8月23日, 2015年2月20日, 2015年4月16日, 2015年5月7日, 2015年5月18日
決議:
Python-Dev 訊息

目錄

重要資訊

此 PEP 為歷史文件。最新且標準的文件現已移至 初始化 C 模組。關於 Python 3.14+,請參閱 定義擴充模組模組定義

×

關於如何提出變更建議,請參閱 PEP 1

摘要

本 PEP 提議重新設計內建模組與擴充模組與匯入機制互動的方式。此機制曾在 Python 3.0 的 PEP 3121 中進行過最後一次修訂,但當時並未解決所有問題。其目標是透過讓擴充模組的行為更接近 Python 模組,來解決與匯入相關的問題;具體而言,是掛載到 PEP 451 引入的基於 ModuleSpec 的載入機制。

本提案借鑒了 PEP 384PyType_Spec,允許擴充模組作者僅定義他們需要的特性,並允許未來對擴充模組宣告進行擴充。

擴充模組以兩步驟過程建立,這更契合 ModuleSpec 架構,並與類別的 __new____init__ 相對應。

擴充模組可以安全地在模組中儲存任意的 C 層級模組專屬狀態,這些狀態受到標準垃圾回收機制保護,並支援重載與子解釋器。鼓勵擴充模組作者在使用新 API 時將這些問題納入考量。

本提案亦允許使用非 ASCII 名稱的擴充模組。

PEP 3121 中處理的問題並未全數在本提案中解決。特別是關於執行時期模組查找(PyState_FindModule)的問題,將留待未來的 PEP 解決。

動機

Python 模組與擴充模組目前的設定方式並不相同。對於 Python 模組,首先會建立並設定模組物件,然後執行模組程式碼(PEP 302)。ModuleSpec 物件(PEP 451)用於保存模組資訊,並傳遞給相關的掛載點(hooks)。

對於擴充模組(即共享函式庫)與內建模組,模組初始化函數會直接執行,同時負責建立與初始化。初始化函數不會收到 ModuleSpec 或其中包含的資訊(如 __file__ 或完整名稱)。這阻礙了相對匯入與資源載入。

在 Python 3 中,模組也不會被加入 sys.modules,這意味著模組的(潛在傳遞性)重新匯入實際上會嘗試再次匯入它,從而導致在執行模組初始化函數時陷入無限迴圈。若沒有對完整模組名稱的存取權,要將模組正確加入 sys.modules 也非易事。這對於 Cython 生成的模組來說尤其成問題,因為其初始化程式碼的複雜度通常與「普通」Python 模組相同。此外,缺乏 __file____name__ 資訊也阻礙了「__init__.py」模組(即套件)的編譯,尤其是在模組初始化時使用相對匯入的情況下。

此外,現有的大多數擴充模組在子解釋器支援和/或解釋器重載方面都存在問題。雖然使用當前基礎設施支援這些功能是可能的,但既不簡單也不高效。解決這些問題是 PEP 3121 的目標,但許多擴充模組(包括標準函式庫中的一些)採取了移植到 Python 3 時阻力最小的做法,導致這些問題未獲解決。本 PEP 保留了向後相容性,這應能減輕壓力,並給予擴充模組作者在移植時考慮這些問題的充足時間。

當前流程

目前,擴充模組與內建模組會匯出一個名為「PyInit_modulename」的初始化函數,該名稱取自共享函式庫的檔案名稱。此函數由匯入機制執行,必須傳回一個已完全初始化的模組物件。由於該函數不接收參數,因此無法得知其匯入環境。

在執行期間,模組初始化函數會根據 PyModuleDef 物件建立一個模組物件。隨後,它會透過向模組字典添加屬性、建立型別等方式繼續進行初始化。

在後端,共享函式庫載入器會記錄最後一個載入模組的完整模組名稱。當建立一個名稱相符的模組時,這個全域變數會被用來確定模組物件的完整名稱。這並不完全安全,因為它依賴於模組初始化函數首先建立自己的模組物件,但這種假設在實務上通常成立。

提案內容

初始化函數(PyInit_modulename)將被允許傳回一個指向 PyModuleDef 物件的指標。匯入機制將負責建構模組物件,並在初始化的相關階段呼叫 PyModuleDef 中提供的掛載點(如下所述)。

這種多階段初始化是一種額外的可能性。目前回傳已完全初始化模組物件的單階段初始化做法仍將被接受,因此現有程式碼將維持不變,包括二進位檔案的相容性。

PyModuleDef 結構將會被修改,包含一個槽位(slots)列表,類似於 PEP 384 的型別 PyType_Spec。為了保持二進位檔案的相容性,並避免引入新的結構(這會引入額外的支援函數與每個模組的儲存空間),PyModuleDef 中目前未使用的 m_reload 指標將被更改為持有這些槽位。這些結構定義如下:

typedef struct {
    int slot;
    void *value;
} PyModuleDef_Slot;

typedef struct PyModuleDef {
    PyModuleDef_Base m_base;
    const char* m_name;
    const char* m_doc;
    Py_ssize_t m_size;
    PyMethodDef *m_methods;
    PyModuleDef_Slot *m_slots;  /* changed from `inquiry m_reload;` */
    traverseproc m_traverse;
    inquiry m_clear;
    freefunc m_free;
} PyModuleDef;

m_slots 成員必須為 NULL,或指向一個 PyModuleDef_Slot 結構陣列,並以 id 設定為 0 的槽位(即 {0, NULL})結尾。

若要指定槽位,必須提供唯一的槽位 ID。Python 的新版本可能會引入新的槽位 ID,但槽位 ID 永遠不會被回收。槽位可能會被棄用,但在整個 Python 3.x 版本中仍會持續支援。

除非在槽位的文件中另有說明,否則槽位的值指標不得為 NULL。

目前可用的槽位如下,稍後將會說明:

  • Py_mod_create
  • Py_mod_exec

未知的槽位 ID 將導致匯入失敗並拋出 SystemError。

使用多階段初始化時,PyModuleDefm_name 欄位將不會在匯入期間使用;模組名稱將從 ModuleSpec 中取得。

在從 PyInit_* 傳回之前,PyModuleDef 物件必須使用新添加的 PyModuleDef_Init 函數進行初始化。此函數會設定物件型別(某些編譯器無法靜態完成此操作)、參考計數與內部簿記資料(m_index)。例如,擴充模組「example」將會匯出為:

static PyModuleDef example_def = {...}

PyMODINIT_FUNC
PyInit_example(void)
{
    return PyModuleDef_Init(&example_def);
}

PyModuleDef 物件必須在由其建立的模組生命週期內保持可用——通常會靜態宣告。

偽代碼概覽

以下是修改後的匯入程式如何運作的概覽。日誌記錄或處理錯誤與無效狀態等細節已省略,且 C 程式碼以簡潔的類 Python 語法呈現。

呼叫匯入程式的框架在 PEP 451 中有說明。

importlib/_bootstrap.py:

class BuiltinImporter:
    def create_module(self, spec):
        module = _imp.create_builtin(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

importlib/_bootstrap_external.py:

class ExtensionFileLoader:
    def create_module(self, spec):
        module = _imp.create_dynamic(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

Python/import.c (_imp 模組)

def create_dynamic(spec):
    name = spec.name
    path = spec.origin

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    return _PyImport_LoadDynamicModuleWithSpec(spec)

def exec_dynamic(module):
    if not isinstance(module, types.ModuleType):
        # non-modules are skipped -- PyModule_GetDef fails on them
        return

    def = PyModule_GetDef(module)
    state = PyModule_GetState(module)
    if state is NULL:
        PyModule_ExecDef(module, def)

def create_builtin(spec):
    name = spec.name

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    for initname, initfunc in PyImport_Inittab:
        if name == initname:
            m = initfunc()
            if isinstance(m, PyModuleDef):
                def = m
                return PyModule_FromDefAndSpec(def, spec)
            else:
                # fall back to single-phase initialization
                module = m
                _PyImport_FixupExtensionObject(module, name, name)
                return module

Python/importdl.c:

def _PyImport_LoadDynamicModuleWithSpec(spec):
    path = spec.origin
    package, dot, name = spec.name.rpartition('.')

    # see the "Non-ASCII module names" section for export_hook_name
    hook_name = export_hook_name(name)

    # call platform-specific function for loading exported function
    # from shared library
    exportfunc = _find_shared_funcptr(hook_name, path)

    m = exportfunc()
    if isinstance(m, PyModuleDef):
        def = m
        return PyModule_FromDefAndSpec(def, spec)

    module = m

    # fall back to single-phase initialization
    ....

Objects/moduleobject.c:

def PyModule_FromDefAndSpec(def, spec):
    name = spec.name
    create = None
    for slot, value in def.m_slots:
        if slot == Py_mod_create:
            create = value
    if create:
        m = create(spec, def)
    else:
        m = PyModule_New(name)

    if isinstance(m, types.ModuleType):
        m.md_state = None
        m.md_def = def

    if def.m_methods:
        PyModule_AddFunctions(m, def.m_methods)
    if def.m_doc:
        PyModule_SetDocString(m, def.m_doc)

def PyModule_ExecDef(module, def):
    if isinstance(module, types.module_type):
        if module.md_state is NULL:
            # allocate a block of zeroed-out memory
            module.md_state = _alloc(module.md_size)

    if def.m_slots is NULL:
        return

    for slot, value in def.m_slots:
        if slot == Py_mod_exec:
            value(module)

模組建立階段

模組物件的建立——即 ExecutionLoader.create_module 的實作——由 Py_mod_create 槽位管理。

Py_mod_create 槽位

Py_mod_create 槽位用於支援自訂模組子類別。值指標必須指向一個具有以下簽名的函數:

PyObject* (*PyModuleCreateFunction)(PyObject *spec, PyModuleDef *def)

該函數接收一個 ModuleSpec 實例(定義於 PEP 451)以及 PyModuleDef 結構。它應傳回一個新的模組物件,或者設定一個錯誤並傳回 NULL。

此函數不負責在該新模組上設定 PEP 451 中指定的匯入相關屬性(如 __name____loader__)。

並不要求傳回的物件必須是 types.ModuleType 的實例。只要該型別支援屬性的設定與獲取(包括至少與匯入相關的屬性),任何型別都可以使用。然而,只有 ModuleType 實例支援模組特定功能,如模組專屬狀態與執行槽位的處理。如果傳回了 ModuleType 子類別以外的物件,則不得定義任何執行槽位;若定義了,將拋出 SystemError

請注意,當此函數被呼叫時,模組在 sys.modules 中的條目尚未填充。再次嘗試匯入同一個模組(可能是傳遞性的)可能會導致無限迴圈。建議擴充模組作者保持 Py_mod_create 盡量簡潔,特別不要從中呼叫使用者程式碼。

不得指定多個 Py_mod_create 槽位。若指定了多個,匯入將會失敗並拋出 SystemError

若未指定 Py_mod_create,匯入機制將使用 PyModule_New 建立一個普通的模組物件。名稱取自 spec

建立後步驟

Py_mod_create 函數傳回 types.ModuleType 或其子類別的實例(或者未提供 Py_mod_create 槽位),匯入機制會將 PyModuleDef 與該模組關聯起來。這也使得 PyModuleDef 可被執行階段、PyModule_GetDef 函數以及垃圾回收常式(traverse, clear, free)存取。

如果 Py_mod_create 函數沒有傳回模組子類別,則 m_size 必須為 0,且 m_traversem_clearm_free 必須均為 NULL。否則,將拋出 SystemError

此外,無論模組物件的型別為何,PyModuleDef 中指定的初始屬性都會被設定在該模組物件上。

  • m_doc 不為 NULL,則文件字串(docstring)將從中設定。
  • 若有定義,模組的函數將從 m_methods 初始化。

模組執行階段

模組執行——即 ExecutionLoader.exec_module 的實作——由「執行槽位」管理。本 PEP 目前僅新增一個 Py_mod_exec,但未來可能會新增其他槽位。

執行階段是在與模組物件關聯的 PyModuleDef 上進行的。對於非 PyModule_Type 子類別的物件(PyModule_GetDef 會對其失敗),執行階段將被跳過。

執行槽位可以指定多次,並依照其在槽位陣列中出現的順序進行處理。使用預設匯入機制時,它們會在 PEP 451 指定的匯入相關屬性(如 __name____loader__)設定完畢且模組加入 sys.modules 後處理。

執行前步驟

在處理執行槽位之前,會為模組分配模組專屬狀態。此後,模組專屬狀態可透過 PyModule_GetState 存取。

Py_mod_exec 槽位

此槽位中的條目必須指向一個具有以下簽名的函數:

int (*PyModuleExecFunction)(PyObject* module)

該函數將被呼叫以初始化模組。通常這意味著設定模組的初始屬性。「module」參數接收要初始化的模組物件。

該函數在成功時必須傳回 0,若發生錯誤則設定一個例外並傳回 -1

PyModuleExec 取代了模組在 sys.modules 中的條目,則 importlib 機制在所有執行槽位處理完畢後,將使用並傳回該新物件。這是匯入機制本身的一個特性。槽位本身全部使用從建立階段傳回的模組進行處理;執行階段期間不會查閱 sys.modules。(請注意,對於擴充模組,實作 Py_mod_create 通常是使用自訂模組物件更好的解決方案。)

舊版初始化 (Legacy Init)

向後相容的單階段初始化將繼續得到支援。在此方案中,PyInit 函數會傳回一個完全初始化的模組,而非 PyModuleDef 物件。在這種情況下,PyInit 掛載點實現了建立階段,而執行階段則是空操作(no-op)。

需要在舊版本 Python 上維持不變運作的模組應堅持使用單階段初始化,因為它所帶來的益處無法向後移植。以下是一個支援多階段初始化的模組範例,並在編譯於舊版 CPython 時回退到單階段初始化。這主要作為說明啟用多階段初始化所需變更的範例:

#include <Python.h>

static int spam_exec(PyObject *module) {
    PyModule_AddStringConstant(module, "food", "spam");
    return 0;
}

#ifdef Py_mod_exec
static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};
#endif

static PyModuleDef spam_def = {
    PyModuleDef_HEAD_INIT,                      /* m_base */
    "spam",                                     /* m_name */
    PyDoc_STR("Utilities for cooking spam"),    /* m_doc */
    0,                                          /* m_size */
    NULL,                                       /* m_methods */
#ifdef Py_mod_exec
    spam_slots,                                 /* m_slots */
#else
    NULL,
#endif
    NULL,                                       /* m_traverse */
    NULL,                                       /* m_clear */
    NULL,                                       /* m_free */
};

PyMODINIT_FUNC
PyInit_spam(void) {
#ifdef Py_mod_exec
    return PyModuleDef_Init(&spam_def);
#else
    PyObject *module;
    module = PyModule_Create(&spam_def);
    if (module == NULL) return NULL;
    if (spam_exec(module) != 0) {
        Py_DECREF(module);
        return NULL;
    }
    return module;
#endif
}

內建模組

任何擴充模組都可以透過連結到執行檔並包含在 inittab 中(無論是在執行時期使用 PyImport_AppendInittab,還是在配置時期使用 freeze 等工具),作為內建模組使用。

為了保持這種可能性,本 PEP 中引入的所有對擴充模組載入的變更也將適用於內建模組。唯一的例外是下面解釋的非 ASCII 模組名稱。

子解釋器與解釋器重載

使用新初始化方案的擴充模組應正確支援子解釋器與多個 Py_Initialize/Py_Finalize 週期,避免 Python 文件 [6] 中提到的問題。該機制旨在讓這一過程變得簡單,但擴充模組作者仍需小心。任何使用者定義的函數、方法或實例都不應洩漏給不同的解釋器。為達成此目的,所有模組層級的狀態應保留在模組字典中,或保留在可透過 PyModule_GetState 存取的模組物件儲存空間中。一個簡單的經驗法則是:除了沒有可變或使用者可設定類別屬性的內建型別外,不要定義任何靜態資料。

與多階段初始化不相容的函數

當在具有非 NULL m_slots 指標的 PyModuleDef 結構上使用時,PyModule_Create 函數將會失敗。該函數無法存取多階段初始化所需的 ModuleSpec 物件。

PyState_FindModule 函數將傳回 NULL,且 PyState_AddModulePyState_RemoveModule 在具有非 NULL m_slots 的模組上也會失敗。PyState 註冊機制已停用,因為可能會有從同一個 PyModuleDef 建立多個模組物件的情況。

模組狀態與 C 層級回呼

由於 PyState_FindModule 無法使用,任何需要存取模組層級狀態(包括模組層級定義的函數、類別或例外)的函數必須直接或間接地接收模組物件(或其需要的特定物件)的參考。這在目前兩種情況下比較困難:

  • 類別的方法:它們接收類別的參考,但不會接收類別所屬模組的參考。
  • 具有 C 層級回呼的函式庫,除非回呼能夠接收在註冊時設定的自訂資料。

修復這些情況超出了本 PEP 的範圍,但這對於讓新機制對所有模組都有用是必要的。關於這些修正的適當方案已在 import-sig 郵件列表 [5] 中進行了討論。

作為一條經驗法則,目前依賴 PyState_FindModule 的模組並不適合移植到新機制。

新函數

將新增一個實現模組建立階段的新函數與巨集。它們類似於 PyModule_CreatePyModule_Create2,差別在於它們接收一個額外的 ModuleSpec 參數,並處理具有非 NULL 槽位的模組定義。

PyObject * PyModule_FromDefAndSpec(PyModuleDef *def, PyObject *spec)
PyObject * PyModule_FromDefAndSpec2(PyModuleDef *def, PyObject *spec,
                                    int module_api_version)

將新增一個實現模組執行階段的新函數。這會分配模組專屬狀態(若尚未分配),並「總是」處理執行槽位。當模組被執行時,匯入機制會呼叫此方法,除非模組正在被重載。

PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def)

將引入另一個函數來初始化 PyModuleDef 物件。此等冪函數(idempotent function)會填入型別、參考計數與模組索引。它會將其參數轉換為 PyObject* 後傳回,因此可以直接從 PyInit 函數中傳回。

PyObject * PyModuleDef_Init(PyModuleDef *);

此外,將新增兩個輔助函數,用於設定模組的文件字串與方法。

int PyModule_SetDocString(PyObject *, const char *)
int PyModule_AddFunctions(PyObject *, PyMethodDef *)

匯出 Hook 名稱

由於可攜式 C 識別符僅限於 ASCII,因此模組名稱必須進行編碼以形成 PyInit 掛載點名稱。

對於 ASCII 模組名稱,匯入掛載點命名為 PyInit_<modulename>,其中 <modulename> 為模組名稱。

對於包含非 ASCII 字元的模組名稱,匯入掛載點命名為 PyInitU_<encodedname>,其中名稱使用 CPython 的「punycode」編碼(Punycode 加上小寫後綴),並將連字號(“-”)替換為底線(“_”)。

在 Python 中

def export_hook_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyInit' + suffix

範例

模組名稱 Init 掛載點名稱
spam PyInit_spam
lančmít PyInitU_lanmt_2sa6t
スパム PyInitU_zck5b2b

對於非 ASCII 名稱的模組,不支援單階段初始化。

在本 PEP 的初始實作中,不支援具有非 ASCII 名稱的內建模組。

模組重載

使用 importlib.reload() 重載擴充模組將持續沒有任何效果,除了重新設定匯入相關屬性外。

由於共享函式庫載入的限制(POSIX 上的 dlopen 與 Windows 上的 LoadModuleEx),通常無法在磁碟上的檔案變更後重新載入修改後的函式庫。

除了嘗試模組的新版本外,重載的使用案例太過罕見,因此無需要求所有模組作者將重載納入考量。如果需要類似重載的功能,作者可以為此匯出專屬函數。

單一函式庫中的多個模組

為了支援在單一共享函式庫中放置多個 Python 模組,函式庫可以匯出除了對應於函式庫檔案名稱之外的額外 PyInit* 符號。

請注意,此機制目前只能用於「載入」額外模組,而不能用於「尋找」它們。(這是載入器機制的限制,本 PEP 不試圖對此進行修改。)為了規避缺乏合適查找器的問題,可以使用如下程式碼:

import importlib.machinery
import importlib.util
loader = importlib.machinery.ExtensionFileLoader(name, path)
spec = importlib.util.spec_from_loader(name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module

在支援符號連結的平台上,可以使用符號連結將同一個函式庫以多個名稱安裝,從而向普通匯入機制公開所有匯出的模組。

測試與初始實作

為了測試,將建立一個新的內建模組 _testmultiphase。該函式庫將使用「單一函式庫中的多個模組」中所述的機制匯出幾個額外的模組。

_testcapi 模組將保持不變,並無限期使用單階段初始化(或直到它不再被支援為止)。

arrayxx* 模組將作為初始實作的一部分,轉換為使用多階段初始化。

API 變更與新增摘要

新函式

  • PyModule_FromDefAndSpec (巨集)
  • PyModule_FromDefAndSpec2
  • PyModule_ExecDef
  • PyModule_SetDocString
  • PyModule_AddFunctions
  • PyModuleDef_Init

新巨集

  • Py_mod_create
  • Py_mod_exec

新型別

  • PyModuleDef_Type 將被公開

新結構

  • PyModuleDef_Slot

其他變更

PyModuleDef.m_reload 變更為 PyModuleDef.m_slots

BuiltinImporterExtensionFileLoader 現在將實作 create_moduleexec_module

內部的 _imp 模組將會有不相容的變更:新增 create_builtincreate_dynamicexec_dynamic;移除 init_builtinload_dynamic

未記錄的函數 imp.load_dynamicimp.init_builtin 將被向後相容的 shims 取代。

回溯相容性

現有模組將繼續與新版本的 Python 保持原始碼與二進位相容。使用多階段初始化的模組將不相容於未實作本 PEP 的 Python 版本。

函數 init_builtinload_dynamic 將從 _imp 模組中移除(但不會從 imp 模組中移除)。

所有已變更的載入器(BuiltinImporterExtensionFileLoader)將保持向後相容;load_module 方法將被一個 shim 取代。

Python/import.c 與 Python/importdl.c 的內部函數將被移除。(具體而言,這些函數為 _PyImport_GetDynLoadFunc_PyImport_GetDynLoadWindows_PyImport_LoadDynamicModule。)

可能的未來擴充

槽位機制借鑒了 PEP 384PyType_Slot,允許未來的擴充。

某些擴充模組匯出了許多常數;例如 _ssl 有長串如下形式的呼叫:

PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
                        PY_SSL_ERROR_ZERO_RETURN);

將其轉換為宣告式列表,類似於 PyMethodDef,將能減少樣板程式碼,並提供通常缺失的自動錯誤檢查功能。

字串常數與型別亦可以類似方式處理。(請注意,非預設的型別基底無法以靜態方式可攜地指定;這種情況將需要一個在添加槽位前執行的 Py_mod_exec 函數。不過,免費的錯誤檢查仍將是有益的。)

另一種可能性是提供一個當模組被傳遞給 Python 的 -m 開關時運行的「main」函數。為此,runpy 模組需要修改以利用 PEP 451 引入的基於 ModuleSpec 的載入方式。此外,還必須增加一種機制,根據模組原本未定義的槽位來設定模組。

實作

正在進行中的實作可在 GitHub 儲存庫 [3] 中找到;補丁集位於 [4]

先前的嘗試

Stefan Behnel 的初始 proto-PEP [1] 具有一個「PyInit_modulename」掛載點,該掛載點會建立一個模組類別,隨後呼叫其 __init__ 來建立模組。該提案並不對應於(當時尚不存在的)PEP 451,在 PEP 451 中模組建立與初始化被分解為不同的步驟。它也不支援將擴充載入到預先存在的模組物件中。

Alyssa (Nick) Coghlan 提議了「Create」與「Exec」掛載點,並撰寫了一個原型實作 [2]。當時 PEP 451 尚未實作,因此該原型並未使用 ModuleSpec。

本 PEP 的原始版本使用了「Create」與「Exec」掛載點,並允許使用「Exec」掛載點載入到任意預先建構的物件中。該提案使擴充模組初始化的方式更接近 Python 模組的初始化方式,但後來發現這並非一個重要的目標。目前的 PEP 描述了一個更簡單的解決方案。

後續迭代使用了「PyModuleExport」掛載點作為 PyInit 的替代方案,其中 PyInit 用於現有方案,而 PyModuleExport 用於多階段方案。然而,無法根據模組名稱確定掛載點名稱,使得像 freeze 這類工具自動生成 PyImport_Inittab 變得複雜。僅保留 PyInit 掛載點名稱,即使對於匯出定義而言並非完全合適,卻產生了一個簡單得多的解決方案。

參考文獻


來源: https://github.com/python/peps/blob/main/peps/pep-0489.rst

最後修改: 2025-10-07 15:05:23 GMT