定義擴充套件模組

CPython 的 C 擴充套件是一個共享庫(例如,Linux 上的 .so 檔案,Windows 上的 .pyd DLL),它可以載入到 Python 程序中(例如,它使用相容的編譯器設定進行編譯),並且匯出一個初始化函式

要預設可匯入(即透過 importlib.machinery.ExtensionFileLoader),共享庫必須在 sys.path 上可用,並且必須以模組名稱加上 importlib.machinery.EXTENSION_SUFFIXES 中列出的副檔名命名。

備註

構建、打包和分發擴充套件模組最好使用第三方工具完成,這超出了本文件的範圍。一個合適的工具是 Setuptools,其文件可在 https://setuptools.pypa.io/en/latest/setuptools.html 找到。

通常,初始化函式返回一個使用 PyModuleDef_Init() 初始化的模組定義。這允許將建立過程分解為幾個階段

  • 在執行任何實質性程式碼之前,Python 可以確定模組支援哪些功能,並且可以調整環境或拒絕載入不相容的擴充套件。

  • 預設情況下,Python 本身建立模組物件——也就是說,它執行與類的 object.__new__() 等效的操作。它還設定了初始屬性,例如 __package____loader__

  • 之後,模組物件使用擴充套件特定程式碼進行初始化——等同於類的 __init__()

這稱為 *多階段初始化*,以區別於遺留(但仍受支援)的 *單階段初始化* 方案,其中初始化函式返回一個完全構建的模組。有關詳細資訊,請參閱下面的單階段初始化部分

3.5 版本更改: 增加了對多階段初始化 (PEP 489) 的支援。

多個模組例項

預設情況下,擴充套件模組不是單例。例如,如果移除了 sys.modules 條目並重新匯入模組,則會建立一個新的模組物件,並且通常會填充新的方法和型別物件。舊模組將進行正常的垃圾回收。這反映了純 Python 模組的行為。

可以在 子直譯器 中或在 Python 執行時重新初始化 (Py_Finalize()Py_Initialize()) 之後建立其他模組例項。在這些情況下,在模組例項之間共享 Python 物件可能會導致崩潰或未定義行為。

為避免此類問題,擴充套件模組的每個例項都應 *隔離*:對一個例項的更改不應隱式影響其他例項,並且模組擁有的所有狀態,包括對 Python 物件的引用,都應特定於特定的模組例項。有關詳細資訊和實用指南,請參閱隔離擴充套件模組

避免這些問題的一種更簡單的方法是在重複初始化時引發錯誤

所有模組都應支援子直譯器,否則應明確表示不支援。這通常透過隔離或阻止重複初始化來實現,如上所述。模組也可以使用 Py_mod_multiple_interpreters 槽限制在主直譯器中。

初始化函式

擴充套件模組定義的初始化函式具有以下簽名

PyObject *PyInit_modulename(void)

其名稱應為 PyInit_<name>,其中 <name> 替換為模組的名稱。

對於僅包含 ASCII 名稱的模組,函式必須命名為 PyInit_<name>,其中 <name> 替換為模組的名稱。使用多階段初始化時,允許使用非 ASCII 模組名稱。在這種情況下,初始化函式名稱為 PyInitU_<name>,其中 <name> 使用 Python 的 punycode 編碼進行編碼,並將連字元替換為下劃線。在 Python 中

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

建議使用輔助宏定義初始化函式

PyMODINIT_FUNC

宣告一個擴充套件模組初始化函式。此宏

  • 指定 PyObject* 返回型別,

  • 新增平臺所需的任何特殊連結宣告,以及

  • 對於 C++,將函式宣告為 extern "C"

例如,名為 spam 的模組將定義如下

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    ...
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

透過定義多個初始化函式,可以從單個共享庫匯出多個模組。但是,匯入它們需要使用符號連結或自定義匯入器,因為預設情況下只找到與檔名對應的函式。有關詳細資訊,請參閱 PEP 489 中的單個庫中的多個模組部分。

初始化函式通常是模組 C 原始檔中定義的唯一非 static 項。

多階段初始化

通常,初始化函式 (PyInit_modulename) 返回一個 PyModuleDef 例項,其 m_slots 不為 NULL。在返回之前,必須使用以下函式初始化 PyModuleDef 例項

PyObject *PyModuleDef_Init(PyModuleDef *def)
自 3.5 版本以來成為 穩定 ABI 的一部分。

確保模組定義是正確初始化的 Python 物件,可以正確報告其型別和引用計數。

返回強制轉換為 PyObject*def,如果發生錯誤則返回 NULL

對於多階段初始化,必須呼叫此函式。它不應在其他上下文中使用。

請注意,Python 假定 PyModuleDef 結構是靜態分配的。此函式可能會返回新引用或借用引用;此引用不得釋放。

在 3.5 版本加入。

遺留的單階段初始化

注意

單階段初始化是初始化擴充套件模組的遺留機制,具有已知的缺點和設計缺陷。鼓勵擴充套件模組作者改用多階段初始化。

在單階段初始化中,初始化函式 (PyInit_modulename) 應建立、填充並返回一個模組物件。這通常使用 PyModule_Create() 和諸如 PyModule_AddObjectRef() 等函式來完成。

單階段初始化與預設初始化方式不同,具體如下:

  • 單階段模組是,或者說 *包含*,“單例”。

    當模組首次初始化時,Python 會儲存模組 __dict__ 的內容(即通常是模組的函式和型別)。

    對於隨後的匯入,Python 不會再次呼叫初始化函式。相反,它會建立一個新的模組物件,其中包含一個新的 __dict__,並將儲存的內容複製到其中。例如,給定一個定義函式 sum 和異常類 error 的單階段模組 _testsinglephase [1]

    >>> import sys
    >>> import _testsinglephase as one
    >>> del sys.modules['_testsinglephase']
    >>> import _testsinglephase as two
    >>> one is two
    False
    >>> one.__dict__ is two.__dict__
    False
    >>> one.sum is two.sum
    True
    >>> one.error is two.error
    True
    

    確切的行為應視為 CPython 實現細節。

  • 為了解決 PyInit_modulename 不接受 spec 引數的事實,匯入機制的某些狀態被儲存並應用於在 PyInit_modulename 呼叫期間建立的第一個合適的模組。具體來說,當匯入子模組時,此機制會將父包名新增到模組名之前。

    一個單階段的 PyInit_modulename 函式應儘快建立“其”模組物件,在任何其他模組物件可以建立之前。

  • 不支援非 ASCII 模組名稱 (PyInitU_modulename)。

  • 單階段模組支援模組查詢函式,例如 PyState_FindModule()