1. 使用 C 或 C++ 擴(kuò)展 Python?
如果你會(huì)用 C,添加新的 Python 內(nèi)置模塊會(huì)很簡單。以下兩件不能用 Python 直接做的事,可以通過 extension modules 來實(shí)現(xiàn):實(shí)現(xiàn)新的內(nèi)置對(duì)象類型;調(diào)用 C 的庫函數(shù)和系統(tǒng)調(diào)用。
為了支持?jǐn)U展,Python API(應(yīng)用程序編程接口)定義了一系列函數(shù)、宏和變量,可以訪問 Python 運(yùn)行時(shí)系統(tǒng)的大部分內(nèi)容。Python 的 API 可以通過在一個(gè) C 源文件中引用 "Python.h"
頭文件來使用。
擴(kuò)展模塊的編寫方式取決與你的目的以及系統(tǒng)設(shè)置;下面章節(jié)會(huì)詳細(xì)介紹。
備注
C擴(kuò)展接口特指CPython,擴(kuò)展模塊無法在其他Python實(shí)現(xiàn)上工作。在大多數(shù)情況下,應(yīng)該避免寫C擴(kuò)展,來保持可移植性。舉個(gè)例子,如果你的用例調(diào)用了C庫或系統(tǒng)調(diào)用,你應(yīng)該考慮使用 ctypes
模塊或 cffi 庫,而不是自己寫C代碼。這些模塊允許你寫Python代碼來接口C代碼,而且可移植性更好。不知為何編譯失敗了。
1.1. 一個(gè)簡單的例子?
讓我們創(chuàng)建一個(gè)擴(kuò)展模塊 spam
(Monty Python 粉絲最喜歡的食物...) 并且想要?jiǎng)?chuàng)建對(duì)應(yīng) C 庫函數(shù) system()
1 的 Python 接口。 這個(gè)函數(shù)接受一個(gè)以 null 結(jié)尾的字符串參數(shù)并返回一個(gè)整數(shù)。 我們希望可以在 Python 中以如下方式調(diào)用此函數(shù):
>>> import spam
>>> status = spam.system("ls -l")
首先創(chuàng)建一個(gè) spammodule.c
文件。(傳統(tǒng)上,如果一個(gè)模塊叫 spam
,則對(duì)應(yīng)實(shí)現(xiàn)它的 C 文件叫 spammodule.c
;如果這個(gè)模塊名字非常長,比如 spammify
,則這個(gè)模塊的文件可以直接叫 spammify.c
。)
文件中開始的兩行是:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
這會(huì)導(dǎo)入 Python API(如果你喜歡,你可以在這里添加描述模塊目標(biāo)和版權(quán)信息的注釋)。
備注
由于 Python 可能會(huì)定義一些能在某些系統(tǒng)上影響標(biāo)準(zhǔn)頭文件的預(yù)處理器定義,因此在包含任何標(biāo)準(zhǔn)頭文件之前,你 必須 先包含 Python.h
。
推薦總是在 Python.h
前定義 PY_SSIZE_T_CLEAN
。查看 提取擴(kuò)展函數(shù)的參數(shù) 來了解這個(gè)宏的更多內(nèi)容。
所有在 Python.h
中定義的用戶可見的符號(hào)都具有 Py
或 PY
前綴,已在標(biāo)準(zhǔn)頭文件中定義的那些除外。 考慮到便利性,也由于其在 Python 解釋器中被廣泛使用,"Python.h"
還包含了一些標(biāo)準(zhǔn)頭文件: <stdio.h>
,<string.h>
,<errno.h>
和 <stdlib.h>
。 如果后面的頭文件在你的系統(tǒng)上不存在,它還會(huì)直接聲明函數(shù) malloc()
,free()
和 realloc()
。
下面添加C函數(shù)到擴(kuò)展模塊,當(dāng)調(diào)用 spam.system(string)
時(shí)會(huì)做出響應(yīng),(我們稍后會(huì)看到調(diào)用):
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
return PyLong_FromLong(sts);
}
有個(gè)直接翻譯參數(shù)列表的方法(舉個(gè)例子,單獨(dú)的 "ls -l"
)到要傳遞給C函數(shù)的參數(shù)。C函數(shù)總是有兩個(gè)參數(shù),通常名字是 self 和 args 。
對(duì)模塊級(jí)函數(shù), self 參數(shù)指向模塊對(duì)象;對(duì)于方法則指向?qū)ο髮?shí)例。
args 參數(shù)是指向一個(gè) Python 的 tuple 對(duì)象的指針,其中包含參數(shù)。 每個(gè) tuple 項(xiàng)對(duì)應(yīng)一個(gè)調(diào)用參數(shù)。 這些參數(shù)也全都是 Python 對(duì)象 --- 要在我們的 C 函數(shù)中使用它們就需要先將其轉(zhuǎn)換為 C 值。 Python API 中的函數(shù) PyArg_ParseTuple()
會(huì)檢查參數(shù)類型并將其轉(zhuǎn)換為 C 值。 它使用模板字符串確定需要的參數(shù)類型以及存儲(chǔ)被轉(zhuǎn)換的值的 C 變量類型。 細(xì)節(jié)將稍后說明。
PyArg_ParseTuple()
在所有參數(shù)都有正確類型且組成部分按順序放在傳遞進(jìn)來的地址里時(shí),返回真(非零)。其在傳入無效參數(shù)時(shí)返回假(零)。在后續(xù)例子里,還會(huì)拋出特定異常,使得調(diào)用的函數(shù)可以理解返回 NULL
(也就是例子里所見)。
1.2. 關(guān)于錯(cuò)誤和異常?
An important convention throughout the Python interpreter is the following: when
a function fails, it should set an exception condition and return an error value
(usually -1
or a NULL
pointer). Exception information is stored in
three members of the interpreter's thread state. These are NULL
if
there is no exception. Otherwise they are the C equivalents of the members
of the Python tuple returned by sys.exc_info()
. These are the
exception type, exception instance, and a traceback object. It is important
to know about them to understand how errors are passed around.
Python API中定義了一些函數(shù)來設(shè)置這些變量。
最常用的就是 PyErr_SetString()
。 其參數(shù)是異常對(duì)象和 C 字符串。 異常對(duì)象一般是像 PyExc_ZeroDivisionError
這樣的預(yù)定義對(duì)象。 C 字符串指明異常原因,并被轉(zhuǎn)換為一個(gè) Python 字符串對(duì)象存儲(chǔ)為異常的“關(guān)聯(lián)值”。
另一個(gè)有用的函數(shù)是 PyErr_SetFromErrno()
,僅接受一個(gè)異常對(duì)象,異常描述包含在全局變量 errno
中。最通用的函數(shù)還是 PyErr_SetObject()
,包含兩個(gè)參數(shù),分別為異常對(duì)象和異常描述。你不需要使用 Py_INCREF()
來增加傳遞到其他函數(shù)的參數(shù)對(duì)象的引用計(jì)數(shù)。
你可以通過 PyErr_Occurred()
在不造成破壞的情況下檢測是否設(shè)置了異常。 這將返回當(dāng)前異常對(duì)象,或者如果未發(fā)生異常則返回 NULL
。 你通常不需要調(diào)用 PyErr_Occurred()
來查看函數(shù)調(diào)用中是否發(fā)生了錯(cuò)誤,因?yàn)槟銘?yīng)該能從返回值中看出來。
當(dāng)一個(gè)函數(shù) f 調(diào)用另一個(gè)函數(shù) g 時(shí)檢測到后者出錯(cuò)了,f 應(yīng)當(dāng)自己返回一個(gè)錯(cuò)誤值 (通常為 NULL
或 -1
)。 它 不應(yīng)該 調(diào)用某個(gè) PyErr_* 函數(shù) --- 這類函數(shù)已經(jīng)被 g 調(diào)用過了。 f 的調(diào)用者隨后也應(yīng)當(dāng)返回一個(gè)錯(cuò)誤來提示 它的 調(diào)用者,同樣 不應(yīng)該 調(diào)用 PyErr_*,依此類推 --- 錯(cuò)誤的最詳細(xì)原因已經(jīng)由首先檢測到它的函數(shù)報(bào)告了。 一旦這個(gè)錯(cuò)誤到達(dá) Python 解釋器的主循環(huán),它會(huì)中止當(dāng)前執(zhí)行的 Python 代碼并嘗試找出由 Python 程序員所指定的異常處理程序。
(在某些情況下,當(dāng)模塊確實(shí)能夠通過調(diào)用其它 PyErr_* 函數(shù)給出更加詳細(xì)的錯(cuò)誤消息,并且在這些情況是可以這樣做的。 但是按照一般規(guī)則,這是不必要的,并可能導(dǎo)致有關(guān)錯(cuò)誤原因的信息丟失:大多數(shù)操作會(huì)由于種種原因而失敗。)
想要忽略由一個(gè)失敗的函數(shù)調(diào)用所設(shè)置的異常,異常條件必須通過調(diào)用 PyErr_Clear()
顯式地被清除。 C 代碼應(yīng)當(dāng)調(diào)用 PyErr_Clear()
的唯一情況是如果它不想將錯(cuò)誤傳給解釋器而是想完全由自己來處理它(可能是嘗試其他方法,或是假裝沒有出錯(cuò))。
每次失敗的 malloc()
調(diào)用必須轉(zhuǎn)換為一個(gè)異常。 malloc()
(或 realloc()
)的直接調(diào)用者必須調(diào)用 PyErr_NoMemory()
來返回錯(cuò)誤來提示。所有對(duì)象創(chuàng)建函數(shù)(例如 PyLong_FromLong()
)已經(jīng)這么做了,所以這個(gè)提示僅用于直接調(diào)用 malloc()
的情況。
還要注意的是,除了 PyArg_ParseTuple()
等重要的例外,返回整數(shù)狀態(tài)碼的函數(shù)通常都是返回正值或零來表示成功,而以 -1
表示失敗,如同 Unix 系統(tǒng)調(diào)用一樣。
最后,當(dāng)你返回一個(gè)錯(cuò)誤指示器時(shí)要注意清理垃圾(通過為你已經(jīng)創(chuàng)建的對(duì)象執(zhí)行 Py_XDECREF()
或 Py_DECREF()
調(diào)用)!
選擇引發(fā)哪個(gè)異常完全取決于你的喜好。 所有內(nèi)置的 Python 異常都有對(duì)應(yīng)的預(yù)聲明 C 對(duì)象,例如 PyExc_ZeroDivisionError
,你可以直接使用它們。 當(dāng)然,你應(yīng)當(dāng)明智地選擇異常 --- 不要使用 PyExc_TypeError
來表示一個(gè)文件無法被打開 (那大概應(yīng)該用 PyExc_IOError
)。 如果參數(shù)列表有問題,PyArg_ParseTuple()
函數(shù)通常會(huì)引發(fā) PyExc_TypeError
。 如果你想要一個(gè)參數(shù)的值必須處于特定范圍之內(nèi)或必須滿足其他條件,則適宜使用 PyExc_ValueError
。
你也可以為你的模塊定義一個(gè)唯一的新異常。需要在文件前部聲明一個(gè)靜態(tài)對(duì)象變量,如:
static PyObject *SpamError;
并且在你的模塊的初始化函數(shù) (PyInit_spam()
) 中使用一個(gè)異常對(duì)象來初始化:
PyMODINIT_FUNC
PyInit_spam(void)
{
PyObject *m;
m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL;
SpamError = PyErr_NewException("spam.error", NULL, NULL);
Py_XINCREF(SpamError);
if (PyModule_AddObject(m, "error", SpamError) < 0) {
Py_XDECREF(SpamError);
Py_CLEAR(SpamError);
Py_DECREF(m);
return NULL;
}
return m;
}
注意異常對(duì)象的Python名字是 spam.error
。而 PyErr_NewException()
函數(shù)可以創(chuàng)建一個(gè)類,其基類為 Exception
(除非是另一個(gè)類傳入以替換 NULL
), 細(xì)節(jié)參見 內(nèi)置異常 。
同樣注意的是創(chuàng)建類保存了 SpamError
的一個(gè)引用,這是有意的。為了防止被垃圾回收掉,否則 SpamError
隨時(shí)會(huì)成為野指針。
一會(huì)討論 PyMODINIT_FUNC
作為函數(shù)返回類型的用法。
spam.error
異??梢栽跀U(kuò)展模塊中拋出,通過 PyErr_SetString()
函數(shù)調(diào)用,如下:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
if (sts < 0) {
PyErr_SetString(SpamError, "System command failed");
return NULL;
}
return PyLong_FromLong(sts);
}
1.3. 回到例子?
回到前面的例子,你應(yīng)該明白下面的代碼:
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
如果在參數(shù)列表中檢測到錯(cuò)誤,它將返回 NULL
(該值是返回對(duì)象指針的函數(shù)所使用的錯(cuò)誤提示),這取決于 PyArg_ParseTuple()
設(shè)置的異常。 在其他情況下參數(shù)的字符串值會(huì)被拷貝到局部變量 command
。 這是一個(gè)指針賦值并且你不應(yīng)該修改它所指向的字符串 (因此在標(biāo)準(zhǔn) C 中,變量 command
應(yīng)當(dāng)被正確地聲明為 const char *command
)。
下一個(gè)語句使用UNIX系統(tǒng)函數(shù) system()
,傳遞給他的參數(shù)是剛才從 PyArg_ParseTuple()
取出的:
sts = system(command);
我們的 spam.system()
函數(shù)必須返回 sts
的值作為Python對(duì)象。這通過使用函數(shù) PyLong_FromLong()
來實(shí)現(xiàn)。
return PyLong_FromLong(sts);
在這種情況下,會(huì)返回一個(gè)整數(shù)對(duì)象,(這個(gè)對(duì)象會(huì)在Python堆里面管理)。
如果你的 C 函數(shù)沒有有用的返回值 (返回 void 的函數(shù)),則對(duì)應(yīng)的 Python 函數(shù)必須返回 None
。 你必須使用這種寫法(可以通過 Py_RETURN_NONE
宏來實(shí)現(xiàn)):
Py_INCREF(Py_None);
return Py_None;
Py_None
是特殊 Python 對(duì)象 None
所對(duì)應(yīng)的 C 名稱。 它是一個(gè)真正的 Python 對(duì)象而不是 NULL
指針,如我們所見,后者在大多數(shù)上下文中都意味著“錯(cuò)誤”。
1.4. 模塊方法表和初始化函數(shù)?
為了展示 spam_system()
如何被Python程序調(diào)用。把函數(shù)聲明為可以被Python調(diào)用,需要先定義一個(gè)方法表 "method table" 。
static PyMethodDef SpamMethods[] = {
...
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
...
{NULL, NULL, 0, NULL} /* Sentinel */
};
注意第三個(gè)參數(shù) ( METH_VARARGS
) ,這個(gè)標(biāo)志指定會(huì)使用C的調(diào)用慣例。可選值有 METH_VARARGS
、 METH_VARARGS | METH_KEYWORDS
。值 0
代表使用 PyArg_ParseTuple()
的陳舊變量。
如果單獨(dú)使用 METH_VARARGS
,函數(shù)會(huì)等待Python傳來tuple格式的參數(shù),并最終使用 PyArg_ParseTuple()
進(jìn)行解析。
METH_KEYWORDS
值表示接受關(guān)鍵字參數(shù)。這種情況下C函數(shù)需要接受第三個(gè) PyObject *
對(duì)象,表示字典參數(shù),使用 PyArg_ParseTupleAndKeywords()
來解析出參數(shù)。
這個(gè)方法表必須被模塊定義結(jié)構(gòu)所引用。
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT,
"spam", /* name of module */
spam_doc, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
SpamMethods
};
這個(gè)結(jié)構(gòu)體必須傳遞給解釋器的模塊初始化函數(shù)。初始化函數(shù)必須命名為 PyInit_name()
,其中 name 是模塊的名字,并應(yīng)該定義為非 static
,且在模塊文件里:
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
注意 PyMODINIT_FUNC 將函數(shù)聲明為 PyObject *
返回類型,聲明了任何平臺(tái)所要求的特殊鏈接聲明,并針對(duì) C++ 將函數(shù)聲明為 extern "C"
。
當(dāng) Python 程序首次導(dǎo)入 spam
模塊時(shí), PyInit_spam()
會(huì)被調(diào)用。 (有關(guān)嵌入 Python 的注釋參見下文。) 它將調(diào)用 PyModule_Create()
,該函數(shù)會(huì)返回一個(gè)模塊對(duì)象,并基于在模塊定義中找到的表將內(nèi)置函數(shù)對(duì)象插入到新創(chuàng)建的模塊中(該表是一個(gè) PyMethodDef
結(jié)構(gòu)體的數(shù)組)。 PyModule_Create()
返回一個(gè)指向它所創(chuàng)建的模塊對(duì)象的指針。 它可能會(huì)因程度嚴(yán)重的特定錯(cuò)誤而中止,或者在模塊無法成功初始化時(shí)返回 NULL
。 初始化函數(shù)必須返回模塊對(duì)象給其調(diào)用者,這樣它就可以被插入到 sys.modules
中。
當(dāng)嵌入Python時(shí), PyInit_spam()
函數(shù)不會(huì)被自動(dòng)調(diào)用,除非放在 PyImport_Inittab
表里。要添加模塊到初始化表,使用 PyImport_AppendInittab()
,可選的跟著一個(gè)模塊的導(dǎo)入。
int
main(int argc, char *argv[])
{
wchar_t *program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
exit(1);
}
/* Add a built-in module, before Py_Initialize */
if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
fprintf(stderr, "Error: could not extend in-built modules table\n");
exit(1);
}
/* Pass argv[0] to the Python interpreter */
Py_SetProgramName(program);
/* Initialize the Python interpreter. Required.
If this step fails, it will be a fatal error. */
Py_Initialize();
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
PyObject *pmodule = PyImport_ImportModule("spam");
if (!pmodule) {
PyErr_Print();
fprintf(stderr, "Error: could not import module 'spam'\n");
}
...
PyMem_RawFree(program);
return 0;
}
備注
要從 sys.modules
刪除實(shí)體或?qū)胍丫幾g模塊到一個(gè)進(jìn)程里的多個(gè)解釋器(或使用 fork()
而沒用 exec()
)會(huì)在一些擴(kuò)展模塊上產(chǎn)生錯(cuò)誤。擴(kuò)展模塊作者可以在初始化內(nèi)部數(shù)據(jù)結(jié)構(gòu)時(shí)給出警告。
更多關(guān)于模塊的現(xiàn)實(shí)的例子包含在Python源碼包的 Modules/xxmodule.c
中。這些文件可以用作你的代碼模板,或者學(xué)習(xí)。腳本 modulator.py 包含在源碼發(fā)行版或Windows安裝中,提供了一個(gè)簡單的GUI,用來聲明需要實(shí)現(xiàn)的函數(shù)和對(duì)象,并且可以生成供填入的模板。腳本在 Tools/modulator/ 目錄。查看README以了解用法。
備注
不像我們的 spam
例子, xxmodule
使用了 多階段初始化 (Python3.5開始引入), PyInit_spam
會(huì)返回一個(gè) PyModuleDef 結(jié)構(gòu)體,然后創(chuàng)建的模塊放到導(dǎo)入機(jī)制。細(xì)節(jié)參考 PEP 489 的多階段初始化。
1.5. 編譯和鏈接?
在你能使用你的新寫的擴(kuò)展之前,你還需要做兩件事情:使用 Python 系統(tǒng)來編譯和鏈接。如果你使用動(dòng)態(tài)加載,這取決于你使用的操作系統(tǒng)的動(dòng)態(tài)加載機(jī)制;更多信息請(qǐng)參考編譯擴(kuò)展模塊的章節(jié)( 構(gòu)建C/C++擴(kuò)展 章節(jié)),以及在 Windows 上編譯需要的額外信息( 在 Windows 上構(gòu)建 C 和 C++ 擴(kuò)展 章節(jié))。
如果你不使用動(dòng)態(tài)加載,或者想要讓模塊永久性的作為Python解釋器的一部分,就必須修改配置設(shè)置,并重新構(gòu)建解釋器。幸運(yùn)的是在Unix上很簡單,只需要把你的文件 ( spammodule.c
為例) 放在解壓縮源碼發(fā)行包的 Modules/
目錄下,添加一行到 Modules/Setup.local
來描述你的文件:
spam spammodule.o
然后在頂層目錄運(yùn)行 make 來重新構(gòu)建解釋器。你也可以在 Modules/
子目錄使用 make,但是你必須先重建 Makefile
文件,然后運(yùn)行 'make Makefile' 命令。(你每次修改 Setup
文件都需要這樣操作。)
如果你的模塊需要額外的鏈接,這些內(nèi)容可以列出在配置文件里,舉個(gè)實(shí)例:
spam spammodule.o -lX11
1.6. 在C中調(diào)用Python函數(shù)?
迄今為止,我們一直把注意力集中于讓Python調(diào)用C函數(shù),其實(shí)反過來也很有用,就是用C調(diào)用Python函數(shù)。這在回調(diào)函數(shù)中尤其有用。如果一個(gè)C接口使用回調(diào),那么就要實(shí)現(xiàn)這個(gè)回調(diào)機(jī)制。
幸運(yùn)的是,Python解釋器是比較方便回調(diào)的,并給標(biāo)準(zhǔn)Python函數(shù)提供了標(biāo)準(zhǔn)接口。(這里就不再詳述解析Python字符串作為輸入的方式,如果有興趣可以參考 Python/pythonmain.c
中的 -c
命令行代碼。)
調(diào)用Python函數(shù)很簡單,首先Python程序要傳遞Python函數(shù)對(duì)象。應(yīng)該提供個(gè)函數(shù)(或其他接口)來實(shí)現(xiàn)。當(dāng)調(diào)用這個(gè)函數(shù)時(shí),用全局變量保存Python函數(shù)對(duì)象的指針,還要調(diào)用 (Py_INCREF()
) 來增加引用計(jì)數(shù),當(dāng)然不用全局變量也沒什么關(guān)系。舉個(gè)例子,如下函數(shù)可能是模塊定義的一部分:
static PyObject *my_callback = NULL;
static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
PyObject *result = NULL;
PyObject *temp;
if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
if (!PyCallable_Check(temp)) {
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
return NULL;
}
Py_XINCREF(temp); /* Add a reference to new callback */
Py_XDECREF(my_callback); /* Dispose of previous callback */
my_callback = temp; /* Remember new callback */
/* Boilerplate to return "None" */
Py_INCREF(Py_None);
result = Py_None;
}
return result;
}
這個(gè)函數(shù)必須使用 METH_VARARGS
標(biāo)志注冊到解釋器,這在 模塊方法表和初始化函數(shù) 章節(jié)會(huì)描述。 PyArg_ParseTuple()
函數(shù)及其參數(shù)的文檔在 提取擴(kuò)展函數(shù)的參數(shù) 。
Py_XINCREF()
和 Py_XDECREF()
這兩個(gè)宏可增加/減少一個(gè)對(duì)象的引用計(jì)數(shù),并且當(dāng)存在 NULL
指針時(shí)仍可保證安全 (但請(qǐng)注意在這個(gè)上下文中 temp 將不為 NULL
)。 更多相關(guān)信息請(qǐng)參考 引用計(jì)數(shù) 章節(jié)。
隨后,當(dāng)要調(diào)用此函數(shù)時(shí),你將調(diào)用 C 函數(shù) PyObject_CallObject()
。 該函數(shù)有兩個(gè)參數(shù),它們都屬于指針,指向任意 Python 對(duì)象:即 Python 函數(shù),及其參數(shù)列表。 參數(shù)列表必須總是一個(gè)元組對(duì)象,其長度即參數(shù)的個(gè)數(shù)量。 要不帶參數(shù)地調(diào)用 Python 函數(shù),則傳入 NULL
或一個(gè)空元組;要帶一個(gè)參數(shù)調(diào)用它,則傳入一個(gè)單元組。 Py_BuildValue()
會(huì)在其格式字符串包含一對(duì)圓括號(hào)內(nèi)的零個(gè)或多個(gè)格式代碼時(shí)返回一個(gè)元組。 例如:
int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
PyObject_CallObject()
返回Python對(duì)象指針,這也是Python函數(shù)的返回值。 PyObject_CallObject()
是一個(gè)對(duì)其參數(shù) "引用計(jì)數(shù)無關(guān)" 的函數(shù)。例子中新的元組創(chuàng)建用于參數(shù)列表,并且在 PyObject_CallObject()
之后立即使用了 Py_DECREF()
。
PyEval_CallObject()
的返回值總是“新”的:要么是一個(gè)新建的對(duì)象;要么是已有對(duì)象,但增加了引用計(jì)數(shù)。所以除非你想把結(jié)果保存在全局變量中,你需要對(duì)這個(gè)值使用 Py_DECREF()
,即使你對(duì)里面的內(nèi)容(特別!)不感興趣。
但是在你這么做之前,很重要的一點(diǎn)是檢查返回值不是 NULL
。 如果是的話,Python 函數(shù)會(huì)終止并引發(fā)異常。 如果調(diào)用 PyObject_CallObject()
的 C 代碼是在 Python 中發(fā)起調(diào)用的,它應(yīng)當(dāng)立即返回一個(gè)錯(cuò)誤來告知其 Python 調(diào)用者,以便解釋器能打印?;厮菪畔?,或者讓調(diào)用方 Python 代碼能處理該異常。 如果這無法做到或不合本意,則應(yīng)當(dāng)通過調(diào)用 PyErr_Clear()
來清除異常。 例如:
if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);
依賴于具體的回調(diào)函數(shù),你還要提供一個(gè)參數(shù)列表到 PyEval_CallObject()
。在某些情況下參數(shù)列表是由Python程序提供的,通過接口再傳到回調(diào)函數(shù)對(duì)象。這樣就可以不改變形式直接傳遞。另外一些時(shí)候你要構(gòu)造一個(gè)新的元組來傳遞參數(shù)。最簡單的方法就是 Py_BuildValue()
函數(shù)構(gòu)造tuple。舉個(gè)例子,你要傳遞一個(gè)事件代碼時(shí)可以用如下代碼:
PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
注意 Py_DECREF(arglist)
所在處會(huì)立即調(diào)用,在錯(cuò)誤檢查之前。當(dāng)然還要注意一些常規(guī)的錯(cuò)誤,比如 Py_BuildValue()
可能會(huì)遭遇內(nèi)存不足等等。
當(dāng)你調(diào)用函數(shù)時(shí)還需要注意,用關(guān)鍵字參數(shù)調(diào)用 PyObject_Call()
,需要支持普通參數(shù)和關(guān)鍵字參數(shù)。有如如上例子中,我們使用 Py_BuildValue()
來構(gòu)造字典。
PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
1.7. 提取擴(kuò)展函數(shù)的參數(shù)?
函數(shù) PyArg_ParseTuple()
的聲明如下:
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);
參數(shù) arg 必須是一個(gè)元組對(duì)象,包含從 Python 傳遞給 C 函數(shù)的參數(shù)列表。format 參數(shù)必須是一個(gè)格式字符串,語法請(qǐng)參考 Python C/API 手冊中的 解析參數(shù)并構(gòu)建值變量。剩余參數(shù)是各個(gè)變量的地址,類型要與格式字符串對(duì)應(yīng)。
注意 PyArg_ParseTuple()
會(huì)檢測他需要的Python參數(shù)類型,卻無法檢測傳遞給他的C變量地址,如果這里出錯(cuò)了,可能會(huì)在內(nèi)存中隨機(jī)寫入東西,小心。
注意任何由調(diào)用者提供的 Python 對(duì)象引用是 借來的 引用;不要遞減它們的引用計(jì)數(shù)!
一些調(diào)用的例子:
#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;
ok = PyArg_ParseTuple(args, ""); /* No arguments */
/* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
/* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
/* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
/* A pair of ints and a string, whose size is also returned */
/* Possible Python call: f((1, 2), 'three') */
{
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
f('spam')
f('spam', 'w')
f('spam', 'wb', 100000) */
}
{
int left, top, right, bottom, h, v;
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
&left, &top, &right, &bottom, &h, &v);
/* A rectangle and a point */
/* Possible Python call:
f(((0, 0), (400, 300)), (10, 10)) */
}
{
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */
}
1.8. 給擴(kuò)展函數(shù)的關(guān)鍵字參數(shù)?
函數(shù) PyArg_ParseTupleAndKeywords()
聲明如下:
int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
const char *format, char *kwlist[], ...);
arg 與 format 形參與 PyArg_ParseTuple()
函數(shù)所定義的一致。 kwdict 形參是作為第三個(gè)參數(shù)從 Python 運(yùn)行時(shí)接收的關(guān)鍵字字典。 kwlist 形參是以 NULL
結(jié)尾的字符串列表,它被用來標(biāo)識(shí)形參;名稱從左至右與來自 format 的類型信息相匹配。 如果執(zhí)行成功,PyArg_ParseTupleAndKeywords()
會(huì)返回真值,否則返回假值并引發(fā)一個(gè)適當(dāng)?shù)漠惓!?/p>
備注
嵌套的元組在使用關(guān)鍵字參數(shù)時(shí)無法生效,不在 kwlist 中的關(guān)鍵字參數(shù)會(huì)導(dǎo)致 TypeError
異常。
如下例子是使用關(guān)鍵字參數(shù)的例子模塊,作者是 Geoff Philbrick (philbrick@hks.com):
#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
int voltage;
const char *state = "a stiff";
const char *action = "voom";
const char *type = "Norwegian Blue";
static char *kwlist[] = {"voltage", "state", "action", "type", NULL};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
&voltage, &state, &action, &type))
return NULL;
printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
action, voltage);
printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);
Py_RETURN_NONE;
}
static PyMethodDef keywdarg_methods[] = {
/* The cast of the function is necessary since PyCFunction values
* only take two PyObject* parameters, and keywdarg_parrot() takes
* three.
*/
{"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
"Print a lovely skit to standard output."},
{NULL, NULL, 0, NULL} /* sentinel */
};
static struct PyModuleDef keywdargmodule = {
PyModuleDef_HEAD_INIT,
"keywdarg",
NULL,
-1,
keywdarg_methods
};
PyMODINIT_FUNC
PyInit_keywdarg(void)
{
return PyModule_Create(&keywdargmodule);
}
1.9. 構(gòu)造任意值?
這個(gè)函數(shù)與 PyArg_ParseTuple()
很相似,聲明如下:
PyObject *Py_BuildValue(const char *format, ...);
接受一個(gè)格式字符串,與 PyArg_ParseTuple()
相同,但是參數(shù)必須是原變量的地址指針(輸入給函數(shù),而非輸出)。最終返回一個(gè)Python對(duì)象適合于返回C函數(shù)調(diào)用給Python代碼。
一個(gè)與 PyArg_ParseTuple()
的不同是,后面可能需要的要求返回一個(gè)元組(Python參數(shù)里誒包總是在內(nèi)部描述為元組),比如用于傳遞給其他Python函數(shù)以參數(shù)。 Py_BuildValue()
并不總是生成元組,在多于1個(gè)格式字符串時(shí)會(huì)生成元組,而如果格式字符串為空則返回 None
,一個(gè)參數(shù)則直接返回該參數(shù)的對(duì)象。如果要求強(qiáng)制生成一個(gè)長度為0的元組,或包含一個(gè)元素的元組,需要在格式字符串中加上括號(hào)。
例子(左側(cè)是調(diào)用,右側(cè)是Python值結(jié)果):
Py_BuildValue("") None
Py_BuildValue("i", 123) 123
Py_BuildValue("iii", 123, 456, 789) (123, 456, 789)
Py_BuildValue("s", "hello") 'hello'
Py_BuildValue("y", "hello") b'hello'
Py_BuildValue("ss", "hello", "world") ('hello', 'world')
Py_BuildValue("s#", "hello", 4) 'hell'
Py_BuildValue("y#", "hello", 4) b'hell'
Py_BuildValue("()") ()
Py_BuildValue("(i)", 123) (123,)
Py_BuildValue("(ii)", 123, 456) (123, 456)
Py_BuildValue("(i,i)", 123, 456) (123, 456)
Py_BuildValue("[i,i]", 123, 456) [123, 456]
Py_BuildValue("{s:i,s:i}",
"abc", 123, "def", 456) {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))
1.10. 引用計(jì)數(shù)?
在C/C++語言中,程序員負(fù)責(zé)動(dòng)態(tài)分配和回收堆heap當(dāng)中的內(nèi)存。在C里,通過函數(shù) malloc()
和 free()
來完成。在C++里是操作 new
和 delete
來實(shí)現(xiàn)相同的功能。
每個(gè)由 malloc()
分配的內(nèi)存塊,最終都要由 free()
退回到可用內(nèi)存池里面去。而調(diào)用 free()
的時(shí)機(jī)非常重要,如果一個(gè)內(nèi)存塊忘了 free()
則會(huì)導(dǎo)致內(nèi)存泄漏,這塊內(nèi)存在程序結(jié)束前將無法重新使用。這叫做 內(nèi)存泄漏 。而如果對(duì)同一內(nèi)存塊 free()
了以后,另外一個(gè)指針再次訪問,則再次使用 malloc()
復(fù)用這塊內(nèi)存會(huì)導(dǎo)致沖突。這叫做 野指針 。等同于使用未初始化的數(shù)據(jù),core dump,錯(cuò)誤結(jié)果,神秘的崩潰等。
內(nèi)存泄露往往發(fā)生在一些并不常見的代碼流程上面。比如一個(gè)函數(shù)申請(qǐng)了內(nèi)存以后,做了些計(jì)算,然后釋放內(nèi)存塊?,F(xiàn)在一些對(duì)函數(shù)的修改可能增加對(duì)計(jì)算的測試并檢測錯(cuò)誤條件,然后過早的從函數(shù)返回了。這很容易忘記在退出前釋放內(nèi)存,特別是后期修改的代碼。這種內(nèi)存泄漏,一旦引入,通常很長時(shí)間都難以檢測到,錯(cuò)誤退出被調(diào)用的頻度較低,而現(xiàn)代電腦又有非常巨大的虛擬內(nèi)存,所以泄漏僅在長期運(yùn)行或頻繁調(diào)用泄漏函數(shù)時(shí)才會(huì)變得明顯。因此,有必要避免內(nèi)存泄漏,通過代碼規(guī)范會(huì)策略來最小化此類錯(cuò)誤。
Python通過 malloc()
和 free()
包含大量的內(nèi)存分配和釋放,同樣需要避免內(nèi)存泄漏和野指針。他選擇的方法就是 引用計(jì)數(shù) 。其原理比較簡單:每個(gè)對(duì)象都包含一個(gè)計(jì)數(shù)器,計(jì)數(shù)器的增減與對(duì)象引用的增減直接相關(guān),當(dāng)引用計(jì)數(shù)為0時(shí),表示對(duì)象已經(jīng)沒有存在的意義了,對(duì)象就可以刪除了。
另一個(gè)叫法是 自動(dòng)垃圾回收 。(有時(shí)引用計(jì)數(shù)也被看作是垃圾回收策略,于是這里的"自動(dòng)"用以區(qū)分兩者)。自動(dòng)垃圾回收的優(yōu)點(diǎn)是用戶不需要明確的調(diào)用 free()
。(另一個(gè)優(yōu)點(diǎn)是改善速度或內(nèi)存使用,然而這并不難)。缺點(diǎn)是對(duì)C,沒有可移植的自動(dòng)垃圾回收器,而引用計(jì)數(shù)則可以可移植的實(shí)現(xiàn)(只要 malloc()
和 free()
函數(shù)是可用的,這也是C標(biāo)準(zhǔn)擔(dān)保的)。也許以后有一天會(huì)出現(xiàn)可移植的自動(dòng)垃圾回收器,但在此前我們必須與引用計(jì)數(shù)一起工作。
Python使用傳統(tǒng)的引用計(jì)數(shù)實(shí)現(xiàn),也提供了循環(huán)監(jiān)測器,用以檢測引用循環(huán)。這使得應(yīng)用無需擔(dān)心直接或間接的創(chuàng)建了循環(huán)引用,這是引用計(jì)數(shù)垃圾收集的一個(gè)弱點(diǎn)。引用循環(huán)是對(duì)象(可能直接)的引用了本身,所以循環(huán)中的每個(gè)對(duì)象的引用計(jì)數(shù)都不是0。典型的引用計(jì)數(shù)實(shí)現(xiàn)無法回收處于引用循環(huán)中的對(duì)象,或者被循環(huán)所引用的對(duì)象,哪怕沒有循環(huán)以外的引用了。
循環(huán)檢測器能夠檢測垃圾回收循環(huán)并能回收它們。 gc
模塊提供了一種運(yùn)行該檢測器的方式 (collect()
函數(shù)),以及多個(gè)配置接口和在運(yùn)行時(shí)禁用該檢測器的功能。
1.10.1. Python中的引用計(jì)數(shù)?
有兩個(gè)宏 Py_INCREF(x)
和 Py_DECREF(x)
,會(huì)處理引用計(jì)數(shù)的增減。 Py_DECREF()
也會(huì)在引用計(jì)數(shù)到達(dá)0時(shí)釋放對(duì)象。為了靈活,并不會(huì)直接調(diào)用 free()
,而是通過對(duì)象的 類型對(duì)象 的函數(shù)指針來調(diào)用。為了這個(gè)目的(或其他的),每個(gè)對(duì)象同時(shí)包含一個(gè)指向自身類型對(duì)象的指針。
最大的問題依舊:何時(shí)使用 Py_INCREF(x)
和 Py_DECREF(x)
?我們首先引入一些概念。沒有人"擁有"一個(gè)對(duì)象,你可以 擁有一個(gè)引用 到一個(gè)對(duì)象。一個(gè)對(duì)象的引用計(jì)數(shù)定義為擁有引用的數(shù)量。引用的擁有者有責(zé)任調(diào)用 Py_DECREF()
,在引用不再需要時(shí)。引用的擁有關(guān)系可以被傳遞。有三種辦法來處置擁有的引用:傳遞、存儲(chǔ)、調(diào)用 Py_DECREF()
。忘記處置一個(gè)擁有的引用會(huì)導(dǎo)致內(nèi)存泄漏。
還可以 借用 2 一個(gè)對(duì)象的引用。借用的引用不應(yīng)該調(diào)用 Py_DECREF()
。借用者必須確保不能持有對(duì)象超過擁有者借出的時(shí)間。在擁有者處置對(duì)象后使用借用的引用是有風(fēng)險(xiǎn)的,應(yīng)該完全避免 3 。
借用相對(duì)于引用的優(yōu)點(diǎn)是你無需擔(dān)心整條路徑上代碼的引用,或者說,通過借用你無需擔(dān)心內(nèi)存泄漏的風(fēng)險(xiǎn)。借用的缺點(diǎn)是一些看起來正確代碼上的借用可能會(huì)在擁有者處置后使用對(duì)象。
借用可以變?yōu)閾碛幸?,通過調(diào)用 Py_INCREF()
。 這不會(huì)影響已經(jīng)借出的擁有者的狀態(tài)。 這會(huì)創(chuàng)建一個(gè)新的擁有引用,并給予完全的擁有者責(zé)任(新的擁有者必須恰當(dāng)?shù)奶幹靡?,就像之前的擁有者那樣)?/p>
1.10.2. 擁有規(guī)則?
當(dāng)一個(gè)對(duì)象引用傳遞進(jìn)出一個(gè)函數(shù)時(shí),函數(shù)的接口應(yīng)該指定擁有關(guān)系的傳遞是否包含引用。
大多數(shù)函數(shù)返回一個(gè)對(duì)象的引用,并傳遞引用擁有關(guān)系。通常,所有創(chuàng)建對(duì)象的函數(shù),例如 PyLong_FromLong()
和 Py_BuildValue()
,會(huì)傳遞擁有關(guān)系給接收者。即便是對(duì)象不是真正新的,你仍然可以獲得對(duì)象的新引用。一個(gè)實(shí)例是 PyLong_FromLong()
維護(hù)了一個(gè)流行值的緩存,并可以返回已緩存項(xiàng)目的新引用。
很多另一個(gè)對(duì)象提取對(duì)象的函數(shù),也會(huì)傳遞引用關(guān)系,例如 PyObject_GetAttrString()
。這里的情況不夠清晰,一些不太常用的例程是例外的 PyTuple_GetItem()
, PyList_GetItem()
, PyDict_GetItem()
, PyDict_GetItemString()
都是返回從元組、列表、字典里借用的引用。
函數(shù) PyImport_AddModule()
也會(huì)返回借用的引用,哪怕可能會(huì)返回創(chuàng)建的對(duì)象:這個(gè)可能因?yàn)橐粋€(gè)擁有的引用對(duì)象是存儲(chǔ)在 sys.modules
里。
當(dāng)你傳遞一個(gè)對(duì)象引用到另一個(gè)函數(shù)時(shí),通常函數(shù)是借用出去的。如果需要存儲(chǔ),就使用 Py_INCREF()
來變成獨(dú)立的擁有者。這個(gè)規(guī)則有兩個(gè)重要的例外: PyTuple_SetItem()
和 PyList_SetItem()
。這些函數(shù)接受傳遞來的引用關(guān)系,哪怕會(huì)失??!(注意 PyDict_SetItem()
及其同類不會(huì)接受引用關(guān)系,他們是"正常的")。
當(dāng)一個(gè)C函數(shù)被Python調(diào)用時(shí),會(huì)從調(diào)用方傳來的參數(shù)借用引用。調(diào)用者擁有對(duì)象的引用,所以借用的引用生命周期可以保證到函數(shù)返回。只要當(dāng)借用的引用需要存儲(chǔ)或傳遞時(shí),就必須轉(zhuǎn)換為擁有的引用,通過調(diào)用 Py_INCREF()
。
Python調(diào)用從C函數(shù)返回的對(duì)象引用時(shí)必須是擁有的引用---擁有關(guān)系被從函數(shù)傳遞給調(diào)用者。
1.10.3. 危險(xiǎn)的薄冰?
有少數(shù)情況下,借用的引用看起來無害,但卻可能導(dǎo)致問題。這通常是因?yàn)榻忉屍鞯碾[式調(diào)用,并可能導(dǎo)致引用擁有者處置這個(gè)引用。
首先需要特別注意的情況是使用 Py_DECREF()
到一個(gè)無關(guān)對(duì)象,而這個(gè)對(duì)象的引用是借用自一個(gè)列表的元素。舉個(gè)實(shí)例:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}
這個(gè)函數(shù)首先借用一個(gè)引用 list[0]
,然后替換 list[1]
為值 0
,最后打印借用的引用。看起來無害是吧,但卻不是。
我們跟著控制流進(jìn)入 PyList_SetItem()
。列表擁有者引用了其所有成員,所以當(dāng)成員1被替換時(shí),就必須處置原來的成員1?,F(xiàn)在假設(shè)原來的成員1是用戶定義類的實(shí)例,且假設(shè)這個(gè)類定義了 __del__()
方法。如果這個(gè)類實(shí)例的引用計(jì)數(shù)是1,那么處置動(dòng)作就會(huì)調(diào)用 __del__()
方法。
既然是Python寫的, __del__()
方法可以執(zhí)行任意Python代碼。是否可能在 bug()
的 item
廢止引用呢,是的。假設(shè)列表傳遞到 bug()
會(huì)被 __del__()
方法所訪問,就可以執(zhí)行一個(gè)語句來實(shí)現(xiàn) del list[0]
,然后假設(shè)這是最后一個(gè)對(duì)對(duì)象的引用,就需要釋放內(nèi)存,從而使得 item
無效化。
解決方法是,當(dāng)你知道了問題的根源,就容易了:臨時(shí)增加引用計(jì)數(shù)。正確版本的函數(shù)代碼如下:
void
no_bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_INCREF(item);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0);
Py_DECREF(item);
}
這是個(gè)真實(shí)的故事。一個(gè)舊版本的Python包含了這個(gè)bug的變種,而一些人花費(fèi)了大量時(shí)間在C調(diào)試器上去尋找為什么 __del__()
方法會(huì)失敗。
這個(gè)問題的第二種情況是借用的引用涉及線程的變種。通常,Python解釋器里多個(gè)線程無法進(jìn)入對(duì)方的路徑,因?yàn)橛袀€(gè)全局鎖保護(hù)著Python整個(gè)對(duì)象空間。但可以使用宏 Py_BEGIN_ALLOW_THREADS
來臨時(shí)釋放這個(gè)鎖,重新獲取鎖用 Py_END_ALLOW_THREADS
。這通常圍繞在阻塞I/O調(diào)用外,使得其他線程可以在等待I/O期間使用處理器。顯然,如下函數(shù)會(huì)跟之前那個(gè)有一樣的問題:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_BEGIN_ALLOW_THREADS
...some blocking I/O call...
Py_END_ALLOW_THREADS
PyObject_Print(item, stdout, 0); /* BUG! */
}
1.10.4. NULL指針?
通常,接受對(duì)象引用作為參數(shù)的函數(shù)不希望你傳給它們 NULL
指針,并且當(dāng)你這樣做時(shí)將會(huì)轉(zhuǎn)儲(chǔ)核心(或在以后導(dǎo)致核心轉(zhuǎn)儲(chǔ))。 返回對(duì)象引用的函數(shù)通常只在要指明發(fā)生了異常時(shí)才返回 NULL
。 不檢測 NULL
參數(shù)的原因在于這些函數(shù)經(jīng)常要將它們所接收的對(duì)象傳給其他函數(shù) --- 如果每個(gè)函數(shù)都檢測 NULL
,將會(huì)導(dǎo)致大量的冗余檢測而使代碼運(yùn)行得更緩慢。
更好的做法是僅在“源頭”上檢測 NULL
,即在接收到一個(gè)可能為 NULL
的指針,例如來自 malloc()
或是一個(gè)可能引發(fā)異常的函數(shù)的時(shí)候。
Py_INCREF()
和 Py_DECREF()
等宏不會(huì)檢測 NULL
指針 --- 但是,它們的變種 Py_XINCREF()
和 Py_XDECREF()
則會(huì)檢測。
用于檢測特定對(duì)象類型的宏 (Pytype_Check()
) 不會(huì)檢測 NULL
指針 --- 同樣地,有大量代碼會(huì)連續(xù)調(diào)用這些宏來測試一個(gè)對(duì)象是否為幾種不同預(yù)期類型之一,這將會(huì)生成冗余的測試。 不存在帶有 NULL
檢測的變體。
C 函數(shù)調(diào)用機(jī)制會(huì)保證傳給 C 函數(shù)的參數(shù)列表 (本示例中為 args
) 絕不會(huì)為 NULL
--- 實(shí)際上它會(huì)保證其總是為一個(gè)元組 4。
任何時(shí)候?qū)?NULL
指針“泄露”給 Python 用戶都會(huì)是個(gè)嚴(yán)重的錯(cuò)誤。
1.11. 在C++中編寫擴(kuò)展?
還可以在C++中編寫擴(kuò)展模塊,只是有些限制。如果主程序(Python解釋器)是使用C編譯器來編譯和鏈接的,全局或靜態(tài)對(duì)象的構(gòu)造器就不能使用。而如果是C++編譯器來鏈接的就沒有這個(gè)問題。函數(shù)會(huì)被Python解釋器調(diào)用(通常就是模塊初始化函數(shù))必須聲明為 extern "C"
。而是否在 extern "C" {...}
里包含Python頭文件則不是那么重要,因?yàn)槿绻x了符號(hào) __cplusplus
則已經(jīng)是這么聲明的了(所有現(xiàn)代C++編譯器都會(huì)定義這個(gè)符號(hào))。
1.12. 給擴(kuò)展模塊提供C API?
很多擴(kuò)展模塊提供了新的函數(shù)和類型供Python使用,但有時(shí)擴(kuò)展模塊里的代碼也可以被其他擴(kuò)展模塊使用。例如,一個(gè)擴(kuò)展模塊可以實(shí)現(xiàn)一個(gè)類型 "collection" 看起來是沒有順序的。就像是Python列表類型,擁有C API允許擴(kuò)展模塊來創(chuàng)建和維護(hù)列表,這個(gè)新的集合類型可以有一堆C函數(shù)用于給其他擴(kuò)展模塊直接使用。
開始看起來很簡單:只需要編寫函數(shù)(無需聲明為 static
),提供恰當(dāng)?shù)念^文件,以及C API的文檔。實(shí)際上在所有擴(kuò)展模塊都是靜態(tài)鏈接到Python解釋器時(shí)也是可以正常工作的。當(dāng)模塊以共享庫鏈接時(shí),一個(gè)模塊中的符號(hào)定義對(duì)另一個(gè)模塊不可見??梢姷募?xì)節(jié)依賴于操作系統(tǒng),一些系統(tǒng)的Python解釋器使用全局命名空間(例如Windows),有些則在鏈接時(shí)需要一個(gè)嚴(yán)格的已導(dǎo)入符號(hào)列表(一個(gè)例子是AIX),或者提供可選的不同策略(如Unix系列)。即便是符號(hào)是全局可見的,你要調(diào)用的模塊也可能尚未加載。
可移植性需要不能對(duì)符號(hào)可見性做任何假設(shè)。這意味著擴(kuò)展模塊里的所有符號(hào)都應(yīng)該聲明為 static
,除了模塊的初始化函數(shù),來避免與其他擴(kuò)展模塊的命名沖突(在段落 模塊方法表和初始化函數(shù) 中討論) 。這意味著符號(hào)應(yīng)該 必須 通過其他導(dǎo)出方式來供其他擴(kuò)展模塊訪問。
Python提供了一個(gè)特別的機(jī)制來傳遞C級(jí)別信息(指針),從一個(gè)擴(kuò)展模塊到另一個(gè):Capsules。一個(gè)Capsule是一個(gè)Python數(shù)據(jù)類型,會(huì)保存指針( void* )。Capsule只能通過其C API來創(chuàng)建和訪問,但可以像其他Python對(duì)象一樣的傳遞。通常,我們可以指定一個(gè)擴(kuò)展模塊命名空間的名字。其他擴(kuò)展模塊可以導(dǎo)入這個(gè)模塊,獲取這個(gè)名字的值,然后從Capsule獲取指針。
Capsule可以用多種方式導(dǎo)出C API給擴(kuò)展模塊。每個(gè)函數(shù)可以用自己的Capsule,或者所有C API指針可以存儲(chǔ)在一個(gè)數(shù)組里,數(shù)組地址再發(fā)布給Capsule。存儲(chǔ)和獲取指針也可以用多種方式,供客戶端模塊使用。
無論你選擇哪個(gè)方法,正確地為你的 Capsule 命名都很重要。 函數(shù) PyCapsule_New()
接受一個(gè)名稱形參 (const char*);允許你傳入一個(gè) NULL
作為名稱,但我們強(qiáng)烈建議你指定一個(gè)名稱。 正確地命名的 Capsule 提供了一定程序的運(yùn)行時(shí)類型安全;沒有可行的方式能區(qū)分兩個(gè)未命名的 Capsule。
通常來說,Capsule用于暴露C API,其名字應(yīng)該遵循如下規(guī)范:
modulename.attributename
便利函數(shù) PyCapsule_Import()
可以方便的載入通過Capsule提供的C API,僅在Capsule的名字匹配時(shí)。這個(gè)行為為C API用戶提供了高度的確定性來載入正確的C API。
如下例子展示了將大部分負(fù)擔(dān)交由導(dǎo)出模塊作者的方法,適用于常用的庫模塊。其會(huì)存儲(chǔ)所有C API指針(例子里只有一個(gè))在 void 指針的數(shù)組里,并使其值變?yōu)镃apsule。對(duì)應(yīng)的模塊頭文件提供了宏來管理導(dǎo)入模塊和獲取C API指針;客戶端模塊只需要在訪問C API前調(diào)用這個(gè)宏即可。
導(dǎo)出的模塊修改自 spam
模塊,來自 一個(gè)簡單的例子 段落。函數(shù) spam.system()
不會(huì)直接調(diào)用C庫函數(shù) system()
,但一個(gè)函數(shù) PySpam_System()
會(huì)負(fù)責(zé)調(diào)用,當(dāng)然現(xiàn)實(shí)中會(huì)更復(fù)雜些(例如添加 "spam" 到每個(gè)命令)。函數(shù) PySpam_System()
也會(huì)導(dǎo)出給其他擴(kuò)展模塊。
函數(shù) PySpam_System()
是個(gè)純C函數(shù),聲明 static
就像其他地方那樣:
static int
PySpam_System(const char *command)
{
return system(command);
}
函數(shù) spam_system()
按照如下方式修改:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = PySpam_System(command);
return PyLong_FromLong(sts);
}
在模塊開頭,在此行后:
#include <Python.h>
添加另外兩行:
#define SPAM_MODULE
#include "spammodule.h"
#define
用于告知頭文件需要包含給導(dǎo)出的模塊,而不是客戶端模塊。最終,模塊的初始化函數(shù)必須負(fù)責(zé)初始化C API指針數(shù)組:
PyMODINIT_FUNC
PyInit_spam(void)
{
PyObject *m;
static void *PySpam_API[PySpam_API_pointers];
PyObject *c_api_object;
m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL;
/* Initialize the C API pointer array */
PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;
/* Create a Capsule containing the API pointer array's address */
c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);
if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) {
Py_XDECREF(c_api_object);
Py_DECREF(m);
return NULL;
}
return m;
}
注意 PySpam_API
聲明為 static
;此外指針數(shù)組會(huì)在 PyInit_spam()
結(jié)束后消失!
頭文件 spammodule.h
里的一堆工作,看起來如下所示:
#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Header file for spammodule */
/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)
/* Total number of C API pointers */
#define PySpam_API_pointers 1
#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */
static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;
#else
/* This section is used in modules that use spammodule's API */
static void **PySpam_API;
#define PySpam_System \
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])
/* Return -1 on error, 0 on success.
* PyCapsule_Import will set an exception if there's an error.
*/
static int
import_spam(void)
{
PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
return (PySpam_API != NULL) ? 0 : -1;
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(Py_SPAMMODULE_H) */
客戶端模塊必須在其初始化函數(shù)里按順序調(diào)用函數(shù) import_spam()
(或其他宏)才能訪問函數(shù) PySpam_System()
。
PyMODINIT_FUNC
PyInit_client(void)
{
PyObject *m;
m = PyModule_Create(&clientmodule);
if (m == NULL)
return NULL;
if (import_spam() < 0)
return NULL;
/* additional initialization can happen here */
return m;
}
這種方法的主要缺點(diǎn)是,文件 spammodule.h
過于復(fù)雜。當(dāng)然,對(duì)每個(gè)要導(dǎo)出的函數(shù),基本結(jié)構(gòu)是相似的,所以只需要學(xué)習(xí)一次。
最后需要提醒的是Capsule提供了額外的功能,用于存儲(chǔ)在Capsule里的指針的內(nèi)存分配和釋放。細(xì)節(jié)參考 Python/C API參考手冊的章節(jié) Capsule 對(duì)象 和Capsule的實(shí)現(xiàn)(在Python源碼發(fā)行包的 Include/pycapsule.h
和 Objects/pycapsule.c
)。
備注