定義擴充套件模組¶
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
槽限制在主直譯器中。
初始化函式¶
擴充套件模組定義的初始化函式具有以下簽名
其名稱應為 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
建議使用輔助宏定義初始化函式
例如,名為 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()
。