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)都具有 PyPY 前綴,已在標(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ù),通常名字是 selfargs 。

對(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[], ...);

argformat 形參與 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++里是操作 newdelete 來實(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.hObjects/pycapsule.c )。

備注

1

這個(gè)函數(shù)的接口已經(jīng)在標(biāo)準(zhǔn)模塊 os 里了,這里作為一個(gè)簡單而直接的例子。

2

術(shù)語"借用"一個(gè)引用是不完全正確的:擁有者仍然有引用的拷貝。

3

檢查引用計(jì)數(shù)至少為1 沒有用 ,引用計(jì)數(shù)本身可以在已經(jīng)釋放的內(nèi)存里,并有可能被其他對(duì)象所用。

4

當(dāng)你使用 "舊式" 風(fēng)格調(diào)用約定時(shí),這些保證不成立,盡管這依舊存在于很多舊代碼中。