1. 使用 C 或 C++ 擴展 Python?
如果你會用 C,添加新的 Python 內置模塊會很簡單。以下兩件不能用 Python 直接做的事,可以通過 extension modules 來實現:實現新的內置對象類型;調用 C 的庫函數和系統調用。
為了支持擴展,Python API(應用程序編程接口)定義了一系列函數、宏和變量,可以訪問 Python 運行時系統的大部分內容。Python 的 API 可以通過在一個 C 源文件中引用 "Python.h" 頭文件來使用。
擴展模塊的編寫方式取決與你的目的以及系統設置;下面章節會詳細介紹。
注解
C擴展接口特指CPython,擴展模塊無法在其他Python實現上工作。在大多數情況下,應該避免寫C擴展,來保持可移植性。舉個例子,如果你的用例調用了C庫或系統調用,你應該考慮使用 ctypes 模塊或 cffi 庫,而不是自己寫C代碼。這些模塊允許你寫Python代碼來接口C代碼,而且可移植性更好。不知為何編譯失敗了。
1.1. 一個簡單的例子?
讓我們創建一個擴展模塊 spam (Monty Python 粉絲最喜歡的食物...) 并且想要創建對應 C 庫函數 system() 1 的 Python 接口。 這個函數接受一個以 null 結尾的字符串參數并返回一個整數。 我們希望可以在 Python 中以如下方式調用此函數:
>>> import spam
>>> status = spam.system("ls -l")
首先創建一個 spammodule.c 文件。(傳統上,如果一個模塊叫 spam,則對應實現它的 C 文件叫 spammodule.c;如果這個模塊名字非常長,比如 spammify,則這個模塊的文件可以直接叫 spammify.c。)
文件中開始的兩行是:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
這會導入 Python API(如果你喜歡,你可以在這里添加描述模塊目標和版權信息的注釋)。
注解
由于 Python 可能會定義一些能在某些系統上影響標準頭文件的預處理器定義,因此在包含任何標準頭文件之前,你 必須 先包含 Python.h。
推薦總是在 Python.h 前定義 PY_SSIZE_T_CLEAN 。查看 提取擴展函數的參數 來了解這個宏的更多內容。
所有在 Python.h 中定義的用戶可見的符號都具有 Py 或 PY 前綴,已在標準頭文件中定義的那些除外。 考慮到便利性,也由于其在 Python 解釋器中被廣泛使用,"Python.h" 還包含了一些標準頭文件: <stdio.h>,<string.h>,<errno.h> 和 <stdlib.h>。 如果后面的頭文件在你的系統上不存在,它還會直接聲明函數 malloc(),free() 和 realloc()。
下面添加C函數到擴展模塊,當調用 spam.system(string) 時會做出響應,(我們稍后會看到調用):
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);
}
有個直接翻譯參數列表的方法(舉個例子,單獨的 "ls -l" )到要傳遞給C函數的參數。C函數總是有兩個參數,通常名字是 self 和 args 。
對模塊級函數, self 參數指向模塊對象;對于方法則指向對象實例。
args 參數是指向一個 Python 的 tuple 對象的指針,其中包含參數。 每個 tuple 項對應一個調用參數。 這些參數也全都是 Python 對象 --- 要在我們的 C 函數中使用它們就需要先將其轉換為 C 值。 Python API 中的函數 PyArg_ParseTuple() 會檢查參數類型并將其轉換為 C 值。 它使用模板字符串確定需要的參數類型以及存儲被轉換的值的 C 變量類型。 細節將稍后說明。
PyArg_ParseTuple() 在所有參數都有正確類型且組成部分按順序放在傳遞進來的地址里時,返回真(非零)。其在傳入無效參數時返回假(零)。在后續例子里,還會拋出特定異常,使得調用的函數可以理解返回 NULL (也就是例子里所見)。
1.2. 關于錯誤和異常?
在 Python 解釋器中有一個重要的慣例:當一個函數出錯時,它應當設置異常條件并返回錯誤值(通常為 NULL 指針)。 異常存儲于解釋器內部的靜態全局變量中;如此變量為 NULL 表示未發生異常。 還有第二個全局變量用于保存異常的“關聯值”(即 raise 的第二個參數)。 第三個變量包含 Python 代碼產生錯誤情況下的棧回溯信息。 這三個變量是 Python 中 sys.exc_info() 的結果在 C 中的對應物(請參閱 Python 庫參考的 sys 模塊部分)。 了解它們對于理解錯誤的傳遞方式是非常重要的。
Python API中定義了一些函數來設置這些變量。
最常用的就是 PyErr_SetString()。 其參數是異常對象和 C 字符串。 異常對象一般是像 PyExc_ZeroDivisionError 這樣的預定義對象。 C 字符串指明異常原因,并被轉換為一個 Python 字符串對象存儲為異常的“關聯值”。
另一個有用的函數是 PyErr_SetFromErrno() ,僅接受一個異常對象,異常描述包含在全局變量 errno 中。最通用的函數還是 PyErr_SetObject() ,包含兩個參數,分別為異常對象和異常描述。你不需要使用 Py_INCREF() 來增加傳遞到其他函數的參數對象的引用計數。
你可以通過 PyErr_Occurred() 在不造成破壞的情況下檢測是否設置了異常。 這將返回當前異常對象,或者如果未發生異常則返回 NULL。 你通常不需要調用 PyErr_Occurred() 來查看函數調用中是否發生了錯誤,因為你應該能從返回值中看出來。
當一個函數 f 調用另一個函數 g 時檢測到后者出錯了,f 應當自己返回一個錯誤值 (通常為 NULL 或 -1)。 它 不應該 調用某個 PyErr_*() 函數 --- 這類函數已經被 g 調用過了。 f 的調用者隨后也應當返回一個錯誤來提示 它的 調用者,同樣 不應該 調用 PyErr_*(),依此類推 --- 錯誤的最詳細原因已經由首先檢測到它的函數報告了。 一旦這個錯誤到達 Python 解釋器的主循環,它會中止當前執行的 Python 代碼并嘗試找出由 Python 程序員所指定的異常處理程序。
(在某些情況下,當模塊確實能夠通過調用其它 PyErr_*() 函數給出更加詳細的錯誤消息,并且在這些情況是可以這樣做的。 但是按照一般規則,這是不必要的,并可能導致有關錯誤原因的信息丟失:大多數操作會由于種種原因而失敗。)
想要忽略由一個失敗的函數調用所設置的異常,異常條件必須通過調用 PyErr_Clear() 顯式地被清除。 C 代碼應當調用 PyErr_Clear() 的唯一情況是如果它不想將錯誤傳給解釋器而是想完全由自己來處理它(可能是嘗試其他方法,或是假裝沒有出錯)。
每次失敗的 malloc() 調用必須轉換為一個異常。 malloc() (或 realloc() )的直接調用者必須調用 PyErr_NoMemory() 來返回錯誤來提示。所有對象創建函數(例如 PyLong_FromLong() )已經這么做了,所以這個提示僅用于直接調用 malloc() 的情況。
還要注意的是,除了 PyArg_ParseTuple() 等重要的例外,返回整數狀態碼的函數通常都是返回正值或零來表示成功,而以 -1 表示失敗,如同 Unix 系統調用一樣。
最后,當你返回一個錯誤指示器時要注意清理垃圾(通過為你已經創建的對象執行 Py_XDECREF() 或 Py_DECREF() 調用)!
選擇引發哪個異常完全取決于你的喜好。 所有內置的 Python 異常都有對應的預聲明 C 對象,例如 PyExc_ZeroDivisionError,你可以直接使用它們。 當然,你應當明智地選擇異常 --- 不要使用 PyExc_TypeError 來表示一個文件無法被打開 (那大概應該用 PyExc_IOError)。 如果參數列表有問題,PyArg_ParseTuple() 函數通常會引發 PyExc_TypeError。 如果你想要一個參數的值必須處于特定范圍之內或必須滿足其他條件,則適宜使用 PyExc_ValueError。
你也可以為你的模塊定義一個唯一的新異常。需要在文件前部聲明一個靜態對象變量,如:
static PyObject *SpamError;
并且在你的模塊的初始化函數 (PyInit_spam()) 中使用一個異常對象來初始化:
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;
}
注意異常對象的Python名字是 spam.error 。而 PyErr_NewException() 函數可以創建一個類,其基類為 Exception (除非是另一個類傳入以替換 NULL ), 細節參見 內置異常 。
同樣注意的是創建類保存了 SpamError 的一個引用,這是有意的。為了防止被垃圾回收掉,否則 SpamError 隨時會成為野指針。
一會討論 PyMODINIT_FUNC 作為函數返回類型的用法。
spam.error 異常可以在擴展模塊中拋出,通過 PyErr_SetString() 函數調用,如下:
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. 回到例子?
回到前面的例子,你應該明白下面的代碼:
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
如果在參數列表中檢測到錯誤,它將返回 NULL (該值是返回對象指針的函數所使用的錯誤提示),這取決于 PyArg_ParseTuple() 設置的異常。 在其他情況下參數的字符串值會被拷貝到局部變量 command。 這是一個指針賦值并且你不應該修改它所指向的字符串 (因此在標準 C 中,變量 command 應當被正確地聲明為 const char *command)。
下一個語句使用UNIX系統函數 system() ,傳遞給他的參數是剛才從 PyArg_ParseTuple() 取出的:
sts = system(command);
我們的 spam.system() 函數必須返回 sts 的值作為Python對象。這通過使用函數 PyLong_FromLong() 來實現。
return PyLong_FromLong(sts);
在這種情況下,會返回一個整數對象,(這個對象會在Python堆里面管理)。
如果你的 C 函數沒有有用的返回值 (返回 void 的函數),則對應的 Python 函數必須返回 None。 你必須使用這種寫法(可以通過 Py_RETURN_NONE 宏來實現):
Py_INCREF(Py_None);
return Py_None;
Py_None 是特殊 Python 對象 None 所對應的 C 名稱。 它是一個真正的 Python 對象而不是 NULL 指針,如我們所見,后者在大多數上下文中都意味著“錯誤”。
1.4. 模塊方法表和初始化函數?
為了展示 spam_system() 如何被Python程序調用。把函數聲明為可以被Python調用,需要先定義一個方法表 "method table" 。
static PyMethodDef SpamMethods[] = {
...
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
...
{NULL, NULL, 0, NULL} /* Sentinel */
};
注意第三個參數 ( METH_VARARGS ) ,這個標志指定會使用C的調用慣例。可選值有 METH_VARARGS 、 METH_VARARGS | METH_KEYWORDS 。值 0 代表使用 PyArg_ParseTuple() 的陳舊變量。
如果單獨使用 METH_VARARGS ,函數會等待Python傳來tuple格式的參數,并最終使用 PyArg_ParseTuple() 進行解析。
METH_KEYWORDS 值表示接受關鍵字參數。這種情況下C函數需要接受第三個 PyObject * 對象,表示字典參數,使用 PyArg_ParseTupleAndKeywords() 來解析出參數。
這個方法表必須被模塊定義結構所引用。
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
};
這個結構體必須傳遞給解釋器的模塊初始化函數。初始化函數必須命名為 PyInit_name() ,其中 name 是模塊的名字,并應該定義為非 static ,且在模塊文件里:
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
注意 PyMODINIT_FUNC 將函數聲明為 PyObject * 返回類型,聲明了任何平臺所要求的特殊鏈接聲明,并針對 C++ 將函數聲明為 extern "C"。
當 Python 程序首次導入 spam 模塊時, PyInit_spam() 會被調用。 (有關嵌入 Python 的注釋參見下文。) 它將調用 PyModule_Create(),該函數會返回一個模塊對象,并基于在模塊定義中找到的表將內置函數對象插入到新創建的模塊中(該表是一個 PyMethodDef 結構體的數組)。 PyModule_Create() 返回一個指向它所創建的模塊對象的指針。 它可能會因程度嚴重的特定錯誤而中止,或者在模塊無法成功初始化時返回 NULL。 初始化函數必須返回模塊對象給其調用者,這樣它就可以被插入到 sys.modules 中。
當嵌入Python時, PyInit_spam() 函數不會被自動調用,除非放在 PyImport_Inittab 表里。要添加模塊到初始化表,使用 PyImport_AppendInittab() ,可選的跟著一個模塊的導入。
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 */
PyImport_AppendInittab("spam", PyInit_spam);
/* Pass argv[0] to the Python interpreter */
Py_SetProgramName(program);
/* Initialize the Python interpreter. Required. */
Py_Initialize();
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
PyImport_ImportModule("spam");
...
PyMem_RawFree(program);
return 0;
}
注解
要從 sys.modules 刪除實體或導入已編譯模塊到一個進程里的多個解釋器(或使用 fork() 而沒用 exec() )會在一些擴展模塊上產生錯誤。擴展模塊作者可以在初始化內部數據結構時給出警告。
更多關于模塊的現實的例子包含在Python源碼包的 Modules/xxmodule.c 中。這些文件可以用作你的代碼模板,或者學習。腳本 modulator.py 包含在源碼發行版或Windows安裝中,提供了一個簡單的GUI,用來聲明需要實現的函數和對象,并且可以生成供填入的模板。腳本在 Tools/modulator/ 目錄。查看README以了解用法。
注解
不像我們的 spam 例子, xxmodule 使用了 多階段初始化 (Python3.5開始引入), PyInit_spam 會返回一個 PyModuleDef 結構體,然后創建的模塊放到導入機制。細節參考 PEP 489 的多階段初始化。
1.5. 編譯和鏈接?
在你能使用你的新寫的擴展之前,你還需要做兩件事情:使用 Python 系統來編譯和鏈接。如果你使用動態加載,這取決于你使用的操作系統的動態加載機制;更多信息請參考編譯擴展模塊的章節( 構建C/C++擴展 章節),以及在 Windows 上編譯需要的額外信息( 在Windows平臺編譯C和C++擴展 章節)。
如果你不使用動態加載,或者想要讓模塊永久性的作為Python解釋器的一部分,就必須修改配置設置,并重新構建解釋器。幸運的是在Unix上很簡單,只需要把你的文件 ( spammodule.c 為例) 放在解壓縮源碼發行包的 Modules/ 目錄下,添加一行到 Modules/Setup.local 來描述你的文件:
spam spammodule.o
然后在頂層目錄運行 make 來重新構建解釋器。你也可以在 Modules/ 子目錄使用 make,但是你必須先重建 Makefile 文件,然后運行 'make Makefile' 命令。(你每次修改 Setup 文件都需要這樣操作。)
如果你的模塊需要額外的鏈接,這些內容可以列出在配置文件里,舉個實例:
spam spammodule.o -lX11
1.6. 在C中調用Python函數?
迄今為止,我們一直把注意力集中于讓Python調用C函數,其實反過來也很有用,就是用C調用Python函數。這在回調函數中尤其有用。如果一個C接口使用回調,那么就要實現這個回調機制。
幸運的是,Python解釋器是比較方便回調的,并給標準Python函數提供了標準接口。(這里就不再詳述解析Python字符串作為輸入的方式,如果有興趣可以參考 Python/pythonmain.c 中的 -c 命令行代碼。)
調用Python函數很簡單,首先Python程序要傳遞Python函數對象。應該提供個函數(或其他接口)來實現。當調用這個函數時,用全局變量保存Python函數對象的指針,還要調用 (Py_INCREF()) 來增加引用計數,當然不用全局變量也沒什么關系。舉個例子,如下函數可能是模塊定義的一部分:
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;
}
這個函數必須使用 METH_VARARGS 標志注冊到解釋器,這在 模塊方法表和初始化函數 章節會描述。 PyArg_ParseTuple() 函數及其參數的文檔在 提取擴展函數的參數 。
Py_XINCREF() 和 Py_XDECREF() 這兩個宏可增加/減少一個對象的引用計數,并且當存在 NULL 指針時仍可保證安全 (但請注意在這個上下文中 temp 將不為 NULL)。 更多相關信息請參考 引用計數 章節。
隨后,當要調用此函數時,你將調用 C 函數 PyObject_CallObject()。 該函數有兩個參數,它們都屬于指針,指向任意 Python 對象:即 Python 函數,及其參數列表。 參數列表必須總是一個元組對象,其長度即參數的個數量。 要不帶參數地調用 Python 函數,則傳入 NULL 或一個空元組;要帶一個參數調用它,則傳入一個單元組。 Py_BuildValue() 會在其格式字符串包含一對圓括號內的零個或多個格式代碼時返回一個元組。 例如:
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對象指針,這也是Python函數的返回值。 PyObject_CallObject() 是一個對其參數 "引用計數無關" 的函數。例子中新的元組創建用于參數列表,并且在 PyObject_CallObject() 之后立即使用了 Py_DECREF() 。
PyEval_CallObject() 的返回值總是“新”的:要么是一個新建的對象;要么是已有對象,但增加了引用計數。所以除非你想把結果保存在全局變量中,你需要對這個值使用 Py_DECREF(),即使你對里面的內容(特別!)不感興趣。
但是在你這么做之前,很重要的一點是檢查返回值不是 NULL。 如果是的話,Python 函數會終止并引發異常。 如果調用 PyObject_CallObject() 的 C 代碼是在 Python 中發起調用的,它應當立即返回一個錯誤來告知其 Python 調用者,以便解釋器能打印棧回溯信息,或者讓調用方 Python 代碼能處理該異常。 如果這無法做到或不合本意,則應當通過調用 PyErr_Clear() 來清除異常。 例如:
if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);
依賴于具體的回調函數,你還要提供一個參數列表到 PyEval_CallObject() 。在某些情況下參數列表是由Python程序提供的,通過接口再傳到回調函數對象。這樣就可以不改變形式直接傳遞。另外一些時候你要構造一個新的元組來傳遞參數。最簡單的方法就是 Py_BuildValue() 函數構造tuple。舉個例子,你要傳遞一個事件代碼時可以用如下代碼:
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) 所在處會立即調用,在錯誤檢查之前。當然還要注意一些常規的錯誤,比如 Py_BuildValue() 可能會遭遇內存不足等等。
當你調用函數時還需要注意,用關鍵字參數調用 PyObject_Call() ,需要支持普通參數和關鍵字參數。有如如上例子中,我們使用 Py_BuildValue() 來構造字典。
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. 提取擴展函數的參數?
函數 PyArg_ParseTuple() 的聲明如下:
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);
參數 arg 必須是一個元組對象,包含從 Python 傳遞給 C 函數的參數列表。format 參數必須是一個格式字符串,語法請參考 Python C/API 手冊中的 解析參數并構建值變量。剩余參數是各個變量的地址,類型要與格式字符串對應。
注意 PyArg_ParseTuple() 會檢測他需要的Python參數類型,卻無法檢測傳遞給他的C變量地址,如果這里出錯了,可能會在內存中隨機寫入東西,小心。
注意任何由調用者提供的Python對象引用是 借來的 引用;不要遞減它們的引用計數!
一些調用的例子:
#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. 給擴展函數的關鍵字參數?
函數 PyArg_ParseTupleAndKeywords() 聲明如下:
int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
const char *format, char *kwlist[], ...);
arg 與 format 形參與 PyArg_ParseTuple() 函數所定義的一致。 kwdict 形參是作為第三個參數從 Python 運行時接收的關鍵字字典。 kwlist 形參是以 NULL 結尾的字符串列表,它被用來標識形參;名稱從左至右與來自 format 的類型信息相匹配。 如果執行成功,PyArg_ParseTupleAndKeywords() 會返回真值,否則返回假值并引發一個適當的異常。
注解
嵌套的元組在使用關鍵字參數時無法生效,不在 kwlist 中的關鍵字參數會導致 TypeError 異常。
如下例子是使用關鍵字參數的例子模塊,作者是 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)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. 構造任意值?
這個函數與 PyArg_ParseTuple() 很相似,聲明如下:
PyObject *Py_BuildValue(const char *format, ...);
接受一個格式字符串,與 PyArg_ParseTuple() 相同,但是參數必須是原變量的地址指針(輸入給函數,而非輸出)。最終返回一個Python對象適合于返回C函數調用給Python代碼。
一個與 PyArg_ParseTuple() 的不同是,后面可能需要的要求返回一個元組(Python參數里誒包總是在內部描述為元組),比如用于傳遞給其他Python函數以參數。 Py_BuildValue() 并不總是生成元組,在多于1個格式字符串時會生成元組,而如果格式字符串為空則返回 None ,一個參數則直接返回該參數的對象。如果要求強制生成一個長度為0的元組,或包含一個元素的元組,需要在格式字符串中加上括號。
例子(左側是調用,右側是Python值結果):
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. 引用計數?
在C/C++語言中,程序員負責動態分配和回收堆heap當中的內存。在C里,通過函數 malloc() 和 free() 來完成。在C++里是操作 new 和 delete 來實現相同的功能。
每個由 malloc() 分配的內存塊,最終都要由 free() 退回到可用內存池里面去。而調用 free() 的時機非常重要,如果一個內存塊忘了 free() 則會導致內存泄漏,這塊內存在程序結束前將無法重新使用。這叫做 內存泄漏 。而如果對同一內存塊 free() 了以后,另外一個指針再次訪問,則再次使用 malloc() 復用這塊內存會導致沖突。這叫做 野指針 。等同于使用未初始化的數據,core dump,錯誤結果,神秘的崩潰等。
內存泄露往往發生在一些并不常見的代碼流程上面。比如一個函數申請了內存以后,做了些計算,然后釋放內存塊。現在一些對函數的修改可能增加對計算的測試并檢測錯誤條件,然后過早的從函數返回了。這很容易忘記在退出前釋放內存,特別是后期修改的代碼。這種內存泄漏,一旦引入,通常很長時間都難以檢測到,錯誤退出被調用的頻度較低,而現代電腦又有非常巨大的虛擬內存,所以泄漏僅在長期運行或頻繁調用泄漏函數時才會變得明顯。因此,有必要避免內存泄漏,通過代碼規范會策略來最小化此類錯誤。
Python通過 malloc() 和 free() 包含大量的內存分配和釋放,同樣需要避免內存泄漏和野指針。他選擇的方法就是 引用計數 。其原理比較簡單:每個對象都包含一個計數器,計數器的增減與對象引用的增減直接相關,當引用計數為0時,表示對象已經沒有存在的意義了,對象就可以刪除了。
另一個叫法是 自動垃圾回收 。(有時引用計數也被看作是垃圾回收策略,于是這里的"自動"用以區分兩者)。自動垃圾回收的優點是用戶不需要明確的調用 free() 。(另一個優點是改善速度或內存使用,然而這并不難)。缺點是對C,沒有可移植的自動垃圾回收器,而引用計數則可以可移植的實現(只要 malloc() 和 free() 函數是可用的,這也是C標準擔保的)。也許以后有一天會出現可移植的自動垃圾回收器,但在此前我們必須與引用計數一起工作。
Python使用傳統的引用計數實現,也提供了循環監測器,用以檢測引用循環。這使得應用無需擔心直接或間接的創建了循環引用,這是引用計數垃圾收集的一個弱點。引用循環是對象(可能直接)的引用了本身,所以循環中的每個對象的引用計數都不是0。典型的引用計數實現無法回收處于引用循環中的對象,或者被循環所引用的對象,哪怕沒有循環以外的引用了。
循環探測器可以檢測垃圾循環并回收。 gc 模塊提供了方法運行探測器 ( collect() 函數) ,而且可以在運行時配置禁用探測器。循環探測器被當作可選組件,默認是包含的,也可以在構建時禁用,在Unix平臺(包括Mac OS X)使用 --without-cycle-gc 選項到 configure 腳本。如果循環探測器被禁用, gc 模塊就不可用了。
1.10.1. Python中的引用計數?
有兩個宏 Py_INCREF(x) 和 Py_DECREF(x) ,會處理引用計數的增減。 Py_DECREF() 也會在引用計數到達0時釋放對象。為了靈活,并不會直接調用 free() ,而是通過對象的 類型對象 的函數指針來調用。為了這個目的(或其他的),每個對象同時包含一個指向自身類型對象的指針。
最大的問題依舊:何時使用 Py_INCREF(x) 和 Py_DECREF(x) ?我們首先引入一些概念。沒有人"擁有"一個對象,你可以 擁有一個引用 到一個對象。一個對象的引用計數定義為擁有引用的數量。引用的擁有者有責任調用 Py_DECREF() ,在引用不再需要時。引用的擁有關系可以被傳遞。有三種辦法來處置擁有的引用:傳遞、存儲、調用 Py_DECREF() 。忘記處置一個擁有的引用會導致內存泄漏。
還可以 借用 2 一個對象的引用。借用的引用不應該調用 Py_DECREF() 。借用者必須確保不能持有對象超過擁有者借出的時間。在擁有者處置對象后使用借用的引用是有風險的,應該完全避免 3 。
借用相對于引用的優點是你無需擔心整條路徑上代碼的引用,或者說,通過借用你無需擔心內存泄漏的風險。借用的缺點是一些看起來正確代碼上的借用可能會在擁有者處置后使用對象。
借用可以變為擁有引用,通過調用 Py_INCREF() 。這不會影響已經借出的擁有者的狀態。這回創建一個新的擁有引用,并給予完全的擁有者責任(新的擁有者必須恰當的處置引用,就像之前的擁有者那樣)。
1.10.2. 擁有規則?
當一個對象引用傳遞進出一個函數時,函數的接口應該指定擁有關系的傳遞是否包含引用。
大多數函數返回一個對象的引用,并傳遞引用擁有關系。通常,所有創建對象的函數,例如 PyLong_FromLong() 和 Py_BuildValue() ,會傳遞擁有關系給接收者。即便是對象不是真正新的,你仍然可以獲得對象的新引用。一個實例是 PyLong_FromLong() 維護了一個流行值的緩存,并可以返回已緩存項目的新引用。
很多另一個對象提取對象的函數,也會傳遞引用關系,例如 PyObject_GetAttrString() 。這里的情況不夠清晰,一些不太常用的例程是例外的 PyTuple_GetItem() , PyList_GetItem() , PyDict_GetItem() , PyDict_GetItemString() 都是返回從元組、列表、字典里借用的引用。
函數 PyImport_AddModule() 也會返回借用的引用,哪怕可能會返回創建的對象:這個可能因為一個擁有的引用對象是存儲在 sys.modules 里。
當你傳遞一個對象引用到另一個函數時,通常函數是借用出去的。如果需要存儲,就使用 Py_INCREF() 來變成獨立的擁有者。這個規則有兩個重要的例外: PyTuple_SetItem() 和 PyList_SetItem() 。這些函數接受傳遞來的引用關系,哪怕會失敗!(注意 PyDict_SetItem() 及其同類不會接受引用關系,他們是"正常的")。
當一個C函數被Python調用時,會從調用方傳來的參數借用引用。調用者擁有對象的引用,所以借用的引用生命周期可以保證到函數返回。只要當借用的引用需要存儲或傳遞時,就必須轉換為擁有的引用,通過調用 Py_INCREF() 。
Python調用從C函數返回的對象引用時必須是擁有的引用---擁有關系被從函數傳遞給調用者。
1.10.3. 危險的薄冰?
有少數情況下,借用的引用看起來無害,但卻可能導致問題。這通常是因為解釋器的隱式調用,并可能導致引用擁有者處置這個引用。
首先需要特別注意的情況是使用 Py_DECREF() 到一個無關對象,而這個對象的引用是借用自一個列表的元素。舉個實例:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}
這個函數首先借用一個引用 list[0] ,然后替換 list[1] 為值 0 ,最后打印借用的引用。看起來無害是吧,但卻不是。
我們跟著控制流進入 PyList_SetItem() 。列表擁有者引用了其所有成員,所以當成員1被替換時,就必須處置原來的成員1。現在假設原來的成員1是用戶定義類的實例,且假設這個類定義了 __del__() 方法。如果這個類實例的引用計數是1,那么處置動作就會調用 __del__() 方法。
既然是Python寫的, __del__() 方法可以執行任意Python代碼。是否可能在 bug() 的 item 廢止引用呢,是的。假設列表傳遞到 bug() 會被 __del__() 方法所訪問,就可以執行一個語句來實現 del list[0] ,然后假設這是最后一個對對象的引用,就需要釋放內存,從而使得 item 無效化。
解決方法是,當你知道了問題的根源,就容易了:臨時增加引用計數。正確版本的函數代碼如下:
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);
}
這是個真實的故事。一個舊版本的Python包含了這個bug的變種,而一些人花費了大量時間在C調試器上去尋找為什么 __del__() 方法會失敗。
這個問題的第二種情況是借用的引用涉及線程的變種。通常,Python解釋器里多個線程無法進入對方的路徑,因為有個全局鎖保護著Python整個對象空間。但可以使用宏 Py_BEGIN_ALLOW_THREADS 來臨時釋放這個鎖,重新獲取鎖用 Py_END_ALLOW_THREADS 。這通常圍繞在阻塞I/O調用外,使得其他線程可以在等待I/O期間使用處理器。顯然,如下函數會跟之前那個有一樣的問題:
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指針?
通常,接受對象引用作為參數的函數不希望你傳給它們 NULL 指針,并且當你這樣做時將會轉儲核心(或在以后導致核心轉儲)。 返回對象引用的函數通常只在要指明發生了異常時才返回 NULL。 不檢測 NULL 參數的原因在于這些函數經常要將它們所接收的對象傳給其他函數 --- 如果每個函數都檢測 NULL,將會導致大量的冗余檢測而使代碼運行得更緩慢。
更好的做法是僅在“源頭”上檢測 NULL,即在接收到一個可能為 NULL 的指針,例如來自 malloc() 或是一個可能引發異常的函數的時候。
Py_INCREF() 和 Py_DECREF() 等宏不會檢測 NULL 指針 --- 但是,它們的變種 Py_XINCREF() 和 Py_XDECREF() 則會檢測。
用于檢測特定對象類型的宏 (Pytype_Check()) 不會檢測 NULL 指針 --- 同樣地,有大量代碼會連續調用這些宏來測試一個對象是否為幾種不同預期類型之一,這將會生成冗余的測試。 不存在帶有 NULL 檢測的變體。
C 函數調用機制會保證傳給 C 函數的參數 (本示例中為 args) 絕不會為 NULL --- 實際上它會保證其總是為一個元組 4。
任何時候將 NULL 指針“泄露”給 Python 用戶都會是個嚴重的錯誤。
1.11. 在C++中編寫擴展?
還可以在C++中編寫擴展模塊,只是有些限制。如果主程序(Python解釋器)是使用C編譯器來編譯和鏈接的,全局或靜態對象的構造器就不能使用。而如果是C++編譯器來鏈接的就沒有這個問題。函數會被Python解釋器調用(通常就是模塊初始化函數)必須聲明為 extern "C" 。而是否在 extern "C" {...} 里包含Python頭文件則不是那么重要,因為如果定義了符號 __cplusplus 則已經是這么聲明的了(所有現代C++編譯器都會定義這個符號)。
1.12. 給擴展模塊提供C API?
很多擴展模塊提供了新的函數和類型供Python使用,但有時擴展模塊里的代碼也可以被其他擴展模塊使用。例如,一個擴展模塊可以實現一個類型 "collection" 看起來是沒有順序的。就像是Python列表類型,擁有C API允許擴展模塊來創建和維護列表,這個新的集合類型可以有一堆C函數用于給其他擴展模塊直接使用。
開始看起來很簡單:只需要編寫函數(無需聲明為 static ),提供恰當的頭文件,以及C API的文檔。實際上在所有擴展模塊都是靜態鏈接到Python解釋器時也是可以正常工作的。當模塊以共享庫鏈接時,一個模塊中的符號定義對另一個模塊不可見。可見的細節依賴于操作系統,一些系統的Python解釋器使用全局命名空間(例如Windows),有些則在鏈接時需要一個嚴格的已導入符號列表(一個例子是AIX),或者提供可選的不同策略(如Unix系列)。即便是符號是全局可見的,你要調用的模塊也可能尚未加載。
可移植性需要不能對符號可見性做任何假設。這意味著擴展模塊里的所有符號都應該聲明為 static ,除了模塊的初始化函數,來避免與其他擴展模塊的命名沖突(在段落 模塊方法表和初始化函數 中討論) 。這意味著符號應該 必須 通過其他導出方式來供其他擴展模塊訪問。
Python提供了一個特別的機制來傳遞C級別信息(指針),從一個擴展模塊到另一個:Capsules。一個Capsule是一個Python數據類型,會保存指針( void * )。Capsule只能通過其C API來創建和訪問,但可以像其他Python對象一樣的傳遞。通常,我們可以指定一個擴展模塊命名空間的名字。其他擴展模塊可以導入這個模塊,獲取這個名字的值,然后從Capsule獲取指針。
Capsule可以用多種方式導出C API給擴展模塊。每個函數可以用自己的Capsule,或者所有C API指針可以存儲在一個數組里,數組地址再發布給Capsule。存儲和獲取指針也可以用多種方式,供客戶端模塊使用。
無論你選擇哪個方法,正確地為你的 Capsule 命名都很重要。 函數 PyCapsule_New() 接受一個名稱形參 (const char *);允許你傳入一個 NULL 作為名稱,但我們強烈建議你指定一個名稱。 正確地命名的 Capsule 提供了一定程序的運行時類型安全;沒有可行的方式能區分兩個未命名的 Capsule。
通常來說,Capsule用于暴露C API,其名字應該遵循如下規范:
modulename.attributename
便利函數 PyCapsule_Import() 可以方便的載入通過Capsule提供的C API,僅在Capsule的名字匹配時。這個行為為C API用戶提供了高度的確定性來載入正確的C API。
如下例子展示了將大部分負擔交由導出模塊作者的方法,適用于常用的庫模塊。其會存儲所有C API指針(例子里只有一個)在 void 指針的數組里,并使其值變為Capsule。對應的模塊頭文件提供了宏來管理導入模塊和獲取C API指針;客戶端模塊只需要在訪問C API前調用這個宏即可。
導出的模塊修改自 spam 模塊,來自 一個簡單的例子 段落。函數 spam.system() 不會直接調用C庫函數 system() ,但一個函數 PySpam_System() 會負責調用,當然現實中會更復雜些(例如添加 "spam" 到每個命令)。函數 PySpam_System() 也會導出給其他擴展模塊。
函數 PySpam_System() 是個純C函數,聲明 static 就像其他地方那樣:
static int
PySpam_System(const char *command)
{
return system(command);
}
函數 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 用于告知頭文件需要包含給導出的模塊,而不是客戶端模塊。最終,模塊的初始化函數必須負責初始化C API指針數組:
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 ;此外指針數組會在 PyInit_spam() 結束后消失!
頭文件 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) */
客戶端模塊必須在其初始化函數里按順序調用函數 import_spam() (或其他宏)才能訪問函數 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;
}
這種方法的主要缺點是,文件 spammodule.h 過于復雜。當然,對每個要導出的函數,基本結構是相似的,所以只需要學習一次。
最后需要提醒的是Capsule提供了額外的功能,用于存儲在Capsule里的指針的內存分配和釋放。細節參考 Python/C API參考手冊的章節 膠囊 和Capsule的實現(在Python源碼發行包的 Include/pycapsule.h 和 Objects/pycapsule.c )。
備注
