1. 程式人生 > >(一)用C或C ++擴展(翻譯)

(一)用C或C ++擴展(翻譯)

改變 pos 忽略 後繼 import 實的 調用c函數 threads keep

用C或C ++擴展

如果你知道如何用C語言編程,那麽為Python添加新的內置模塊是很容易的。這種擴展模塊可以做兩件不能直接在Python中完成的事情:它們可以實現新的內置對象類型,以及調用C庫函數和系統調用。

為了支持擴展,Python API(應用程序員接口)定義了一組函數、宏和變量,它們提供對Python運行時系統大部分方面的訪問。Python API通過包含頭文件"Python.h"被合並到C源文件中。

擴展模塊的編譯取決於其預期用途以及系統設置; 細節在後面的章節中給出。

註意:C擴展接口是CPython特有的,擴展模塊不適用於其他Python實現。在許多情況下,可以避免編寫C擴展並保持其他實現的可移植性。例如,如果您的用例調用C庫函數或系統調用,則應考慮使用ctypes模塊或cffi庫,而不是編寫自定義C代碼。這些模塊允許您編寫Python代碼以與C代碼交互,並且在編寫和編譯C擴展模塊時,它們在Python的實現之間更具可移植性。

一個簡單的例子

讓我們創建一個名為spam(Monty Python粉絲最喜歡的食物...)的擴展模塊,並假設我們要為C庫函數system()創建一個Python接口。該函數將一個以空字符結尾的字符串作為參數,並返回一個整數。我們希望這個函數可以從Python中調用,如下所示:

>>> import spam
>>> status = spam.system("ls -l")

首先創建一個文件spammodule.c(根據歷史經驗,如果一個模塊名稱為spam,那麽包含其實現的C文件名稱為spammodule.c;如果模塊名稱很長,比如spammify,那麽模塊名稱可以是spammify.c)

我們文件的第一行是:

#include <Python.h>

它引入了Python API(如果你喜歡,你可以添加一個描述模塊用途的註釋和一個版權聲明)。

註意:由於Python可能會定義一些影響某些系統上標準頭文件的預處理器定義,因此必須在包含Python.h任何標準頭文件之前包含這些定義。

由Python.h所定義的所有用戶可見符號都具有前綴Py或PY,除了在標準頭文件中定義的符號。為了方便起見,"Python.h" 包括一些被Python解釋廣泛使用的標準頭文件:<stdio.h>、<string.h>、 <errno.h>和<stdlib.h>

。如果是後者的頭文件沒有您的系統上存在,它將直接聲明malloc(),free()和 realloc()函數。

接下來我們添加C語言函數到模塊文件中,在使用Python表達式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);
}

Python中的參數列表和C函數的參數列表之間有一個簡單的映射關系(例如單個表達式"ls -l")。C函數總是有兩個參數,通常命名為self和args。

對於模塊級別的函數self變量指向模塊對象,對於對象方法self變量指向對象實例。

ARGS參數指向包含參數的Python元組對象。元組中的每一項都對應於調用參數列表中的參數。參數是Python對象 - 為了在C函數中對它們做任何事情,我們必須將它們轉換為C值。Python API中的PyArg_ParseTuple()函數用於參數類型檢查並將它們轉換為C值。它使用模板字符串來確定參數的所需類型以及存儲轉換值的C變量的類型,稍後再詳細介紹。

如果所有參數都具有正確的類型並且被存儲到傳遞的變量地址中,則PyArg_ParseTuple()返回true(非零)。如果傳遞了一個無效參數列表,它將返回false(零)。在後一種情況下,它也會引發適當的異常,所以調用函數可以立即返回NULL(如我們在示例中所見)。

Intermezzo:錯誤和例外

整個Python解釋器中一個重要的約定如下:當一個函數失敗時,它應該設置一個異常條件並返回一個錯誤值(通常是一個NULL指針)。異常存儲在解釋器內的靜態全局變量中,如果此變量為NULL,則不會發生異常。第二個全局變量存儲異常的"關聯值"(第二個參數raise)。第三個變量包含棧回溯,以防錯誤發生在Python代碼中。這三個變量是Python中sys.exc_info()執行結果的C等價物(請參閱sysPython庫參考中模塊部分)。了解他們了解錯誤是如何傳遞的非常重要。

Python API定義了許多函數來設置各種類型的異常。

最常見的是PyErr_SetString()。它的參數是一個異常對象和一個C字符串:異常對象通常是一個預定義的對象例如PyExc_ZeroDivisionError;C字符串指示錯誤的原因,並轉換為Python字符串對象存入異常的"關聯值"。

另一個有用的函數是PyErr_SetFromErrno(),它只接受一個異常參數,並通過檢查全局變量來構造關聯的值errno。最通用的函數是PyErr_SetObject(),它接受兩個對象參數,異常及其相關的值。您不需要對傳遞給這些函數的對象執行Py_INCREF()。

通過調用PyErr_Occurred()您可以非破壞性地測試是否設置了例外,它將返回當前的異常對象, 如果沒有發生異常,則返回NULL。通常您不需要調用PyErr_Occurred()以查看函數調用是否發生錯誤,因為您應該能夠根據返回值進行分析。

當調用另一個函數g的函數f檢測到後者失敗時,f本身應該返回一個錯誤值(通常為NULL或-1)。它不應該再調用任何PyErr_*()函數 - 因為g已經調用了。f的調用者應該也返回一個錯誤指示它的調用者,同樣不需要再調用任何PyErr_*()函數,然後繼續 - 錯誤的最詳細原因已經由首次檢測到它的函數報告(例如此處的g函數)。一旦錯誤到達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()調用)!

選擇哪個異常來完全取決於你。有預先聲明的C對象與所有內置的Python異常相對應,比如 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_INCREF(SpamError);
    PyModule_AddObject(m, "error", SpamError);
    return m;
}

請註意,異常對象的Python名稱是spam.error。該 PyErr_NewException()函數可以創建一個基類為Exception的類(除非傳入另一個類而不是NULL),如內置異常中所述。

還要註意,該SpamError變量保留對新創建的異常類的引用,這是故意的!由於可以通過外部代碼從模塊中刪除異常,所以需要擁有該類的引用,來確保SpamError不會因為被丟棄,從而成為懸掛指針。如果它成為懸掛指針,引發異常的C代碼可能會導致崩潰或其他意外副作用。

稍後在本示例中我們將討論PyMODINIT_FUNC作為函數返回類型的用法。

在你的擴展模塊中,可以通過調用PyErr_SetString()來引發spam.error異常,如下所示:

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);
}

回到示例

回到我們的示例函數,您現在應該能夠理解這個語句:

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指針,這在大多數情況下都意味著"錯誤"。

模塊的方法表和初始化函數

我承諾展示如何從Python程序中調用spam_system()。首先,我們需要在"方法表"中列出其名稱和地址:

static PyMethodDef SpamMethods[] = {
    ...
    {"system",  spam_system, METH_VARARGS,
     "Execute a shell command."},
    ...
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

請註意第三項(METH_VARARGS)。這是一個標誌,告訴解釋器用於C函數的調用約定。它通常應該是METH_VARARGSMETH_VARARGS | METH_KEYWORDS

僅使用METH_VARARGS時,函數期望將Python級的參數作為能夠被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()被調用。它調用PyModule_Create()返回模塊對象,並根據PyMethodDef模塊定義中的表(結構數組)插入內置函數對象到新創建的模塊中。 PyModule_Create()返回一個指向它創建的模塊對象的指針。如果因致命錯誤而中止或者模塊初始化失敗,則返回NULL。init函數必須將模塊對象返回給其調用者,以便將其插入sys.modules

嵌入Python時,除非PyImport_Inittab表中有對應條目,否則不會自動調用PyInit_spam()函數。要將模塊添加到初始化表中,請使用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。這個文件可以作為模板使用,或者只是作為一個例子閱讀。

註意 與我們的spam示例不同,xxmodule它使用多階段初始化 (Python 3.5中的新增功能),從PyInit_spam中返回PyModuleDef結構 ,並且將模塊創建留給導入機制。

編譯和鏈接

在使用新擴展之前,還有兩件事要做:編譯並將其與Python系統鏈接。如果使用動態加載,細節可能取決於系統使用的動態加載樣式,查閱Building C and C++ Extensions以及Building C and C++ Extensions on Windows獲取更多信息。

如果你不能使用動態加載,或者如果你想讓你的模塊成為Python解釋器的一個永久部分,你將不得不改變配置設置並重建解釋器。幸運的是,在Unix上這非常簡單:只需將您的文件(spammodule.c例如)放入Modules/解壓源代碼發行版的目錄中,然後在Modules/Setup.local描述文件的文件中添加一行 :

spam spammodule.o

並通過在頂級目錄中運行make來重建解釋器。您也可以在Modules/子目錄中運行make,但是必須先通過運行make Makefile來重新編譯Makefile。(每次更改Setup文件時都需要這樣做)

如果您的模塊需要額外的庫進行鏈接,這些庫也可以在配置文件的行中列出,例如:

spam spammodule.o -lX11

從C調用Python函數

到目前為止,我們已經集中在使C函數可以從Python調用。反過來也很有用:從C中調用Python函數。對於支持所謂的"回調"函數的庫尤其如此。如果C接口使用回調函數,等效的Python通常需要為Python程序員提供回調機制,該實現將需要從C回調調用Python回調函數。其他用途也是可以想象的。

幸運的是,Python解釋器很容易被遞歸調用,並且有一個標準的接口來調用Python函數。(我不會詳細討論如何使用特定的字符串作為輸入來調用Python解析器 - 如果您有興趣,請查看Python源代碼中的-c命令行選項的實現Modules/main.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標誌向解釋器註冊,這在The Module’s Method Table and Initialization Function一節中有描述。PyArg_ParseTuple()函數及其參數說明記錄在Extracting Parameters in Extension Functions。

宏Py_XINCREF()和Py_XDECREF()用來增加/減少一個對象的引用計數,即使是NULL指針也是安全的(但請註意,在這種情況下temp不會為NULL)。有關它們的更多信息,請參閱參考計數部分。

稍後,當需要調用該函數的時候,您可以使用C函數PyObject_CallObject()。這個函數有兩個參數,都是指向任意Python對象的指針:Python函數和參數列表。參數列表必須始終是一個元組對象,其長度是參數個數。調用沒有參數的Python函數時,傳入NULL或空元組; 調用一個參數的Python函數時,傳遞一個單例元組。當其格式字符串由零或更多格式代碼組成時,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()後通過Py_DECREF調用被立即釋放。

返回值PyObject_CallObject()是"new":它是一個全新的對象或者它是引用計數已遞增的現有對象,所以除非你想把它保存在一個全局變量中,否則你應該以某種方式對得到的結果執行Py_DECREF(),甚至(尤其是!)如果你對它的價值不感興趣。

但是,在執行此操作之前,檢查返回值是否非空非常重要。如果為空,則通過引發異常終止Python函數。如果是從Python調用的C代碼PyObject_CallObject(),應該向其Python調用者返回錯誤指示,以便解釋器可以打印堆棧跟蹤或者調用可以處理異常的Python代碼。如果這不可能或不可取,應通過調用清除異常PyErr_Clear()。例如:

if (result == NULL)
    return NULL; /* Pass error back */
...use result...
Py_DECREF(result);

根據Python回調函數接口,您可能還需要提供參數列表給PyObject_CallObject()。您可能需要構造一個新的元組作為參數列表,最簡單的方法就是調用Py_BuildValue()。例如您想傳遞一個整型事件編碼,則可以使用以下代碼:

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);

在擴展函數中提取參數

PyArg_ParseTuple()函數聲明如下:

int PyArg_ParseTuple(PyObject *arg, const char *format, ...);

ARG參數必須是包含Python傳遞給C函數的參數列表的元組對象。format參數必須是一個格式字符串,其語法在Python/C API參考手冊Parsing arguments and building values中給出。其余的參數必須是變量的地址,其類型由格式字符串決定。

請註意,雖然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) */
}

擴展函數的關鍵字參數

PyArg_ParseTupleAndKeywords()函數聲明如下:

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char *kwlist[], ...);

arg和format參數含義同PyArg_ParseTuple()函數,kwdict參數用來接收來自Python運行時的第三參數的關鍵字詞典,kwlist參數是以NULL結尾用於標識參數的字符串列表。成功時PyArg_ParseTupleAndKeywords()返回true,否則返回false,並引發異常。

註意:使用關鍵字參數時不能分析嵌套元組!傳入的關鍵字參數在kwlist不存在會導致TypeError異常。

下面是一個使用關鍵字的例子:

#include "Python.h"

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    char *state = "a stiff";
    char *action = "voom";
    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);
}

建立任意值

函數PyArg_ParseTuple()聲明如下:

PyObject *Py_BuildValue(const char *format, ...);

它識別一組類似於被PyArg_ParseTuple()識別的format單元,但參數(輸入到函數而不是輸出)不能是指針,只能是值。它返回一個新的Python對象,適合從Python調用的C函數中返回。

與PyArg_ParseTuple()不同的是:後者要求其第一個參數是一個元組(因為Python參數列表總是在內部表示為元組),Py_BuildValue()並不總是構建一個元組。它僅在格式字符串包含兩個或更多格式單元時才構建元組。如果格式字符串為空,則返回None; 如果它只包含一個格式單元,則返回該格式單元描述的任何對象。要強制它返回一個大小為0或1的元組,使用括號包裹format字符串。

示例(在左邊的調用,右邊的結果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))

參考計數

在C或C++等語言中,程序員負責動態分配和釋放堆上的內存,在C中使用函數malloc()和free(),在C++中使用new和delete。我們將限制以下討論到C的情況下。

每個分配的內存塊malloc()最終都應該通過一次free()調用返還到可用內存池。在正確的時間調用free()很重要。如果一個塊的地址被遺忘,但是free()沒有被調用,它占用的內存將不能被重用,直到程序終止,這被稱為內存泄漏。另一方面,如果一個程序繼續使用一個已經被調用過free()的內存塊,另一個malloc()調用可能產生與該塊重用的沖突,這被稱為使用釋放的內存。它與引用未初始化的數據有相同的不良後果 - 錯誤的結果,神秘的崩潰。

內存泄漏的常見原因是代碼執行了一段的不尋常的路徑。例如,一個函數可以分配一塊內存,做一些計算,然後釋放該塊。現在如果為函數的計算添加一段錯誤檢測邏輯,並且可能會導致函數提前返回。在過早退出時忘記釋放分配的內存塊是很容易的,特別是當退出邏輯是後面添加到代碼中的時候。這種泄漏一旦被引入,往往不會被長時間檢測出來:錯誤退出只占所有調用的一小部分,而大多數現代機器都有大量的虛擬內存,所以在長時間運行的過程中頻繁調用問題函數才會導致明顯的內存泄露。因此通過編碼慣例或策略來最大限度地減少這類錯誤,防止泄漏發生是非常重要的。

由於Python大量使用malloc()和free(),它需要一種策略來避免內存泄漏以及釋放內存的使用。所選的方法稱為參考計數。原理很簡單:每個對象都包含一個計數器,當某個對象的引用存儲在某個地方時,該計數器會遞增,而當對該引用的引用被刪除時該計數器遞減。當計數器達到零時,對象的最後一個引用已被刪除,對象被釋放。

另一種策略稱為自動垃圾收集。(有時引用計數也被稱為垃圾收集策略,因此我使用"自動"來區分兩者)自動垃圾收集的一大優勢是用戶無需顯式調用free() 。(另一個聲稱的優點是速度或內存使用方面的改進 - 但這並不難)。缺點是對於C,沒有真正的便攜式自動垃圾收集器,而引用計數可以實現可移植性(只要函數malloc() 並且free()可用 - C標準保證)。也許有一天,一個足夠便攜的自動垃圾收集器將可用於C,在那之前我們必須忍受引用計數。

當Python使用傳統的引用計數實現時,它還提供了一個循環檢測器,用於檢測循環引用。這允許應用程序不用擔心創建直接或間接循環引用,而這是僅使用引用計數實現的垃圾收集的弱點。循環引用由包含(可能間接)引用自身的對象組成,因此循環中的每個對象都有一個非零的引用計數。典型的引用計數實現不能回收任何處於循環引用中的對象內存或者引用,即使循環對象本身不再有進一步的引用。

循環檢測器能夠檢測垃圾循環並可以回收它們。該gc模塊公開了一種運行探測器(collect()功能)的方法,以及配置接口和在運行時禁用探測器的功能。循環檢測器被視為可選組件,雖然它是默認包含的,但它可以在構建時使用Unix平臺(包括Mac OS X)上--without-cycle-gc的configure腳本選項來禁用,如果循環檢測器以這種方式被禁用,gc模塊將不可用。

Python中的引用計數

宏Py_INCREF(x)和Py_DECREF(x)用來處理引用計數的遞增和遞減。Py_DECREF()當計數達到零時也釋放對象。為了靈活性,它不直接調用free() - 而是通過調用類型對象中的函數指針,為此(和其他)每個對象還包含一個指向其類型對象的指針。

現在最大的問題仍然是:何時使用Py_INCREF(x)和Py_DECREF(x)?我們先來介紹一些術語。沒有人"擁有"一個物體,但是您可以擁有對象的引用。現在對象的引用計數定義為擁有它的引用的數量。當不再需要引用時,引用的所有者負責調用Py_DECREF()。引用的所有權可以轉移。有三種方式可以處理擁有的引用:傳遞它、存儲它或調用Py_DECREF(),忘記處理擁有的引用會造成內存泄漏。

也可以借用對象的引用,引用的借用者不應該調用Py_DECREF(),引用的借用者不能比被借用者持有對象時間更長。被借用者釋放對象後,借用者繼續使用對象會有風險,應該完全避免。

借用引用的優勢在於,你不需要關心引用的處理,不會因為提前返回導致內存泄露,缺點就是可能存在使用已經釋放了的對象。

借用引用可以通過調用Py_INCREF()變成引用擁有者,這不會影響借用出引用的擁有者的地位 - 它會創建一個新的擁有引用,並給予全部的擁有責任(新擁有者必須正確處理引用就像之前的擁有者那樣)。

所有權規則

無論何時將對象引用傳入或傳出函數,它都是函數接口規範的一部分,而不管所有權是否與引用一起傳輸。

大多數返回對象引用的函數都會將引用的所有權傳遞給它,特別是創建新對象的函數,例如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()不會接管所有權 - 他們是"正常"的)

當從Python調用C函數時,它會借用調用者的參數的引用。調用者擁有對該對象的引用,所以借用的引用的生命周期將得到保證,直到該函數返回。只有當借用引用必須被存儲或傳遞時,必須通過調用Py_INCREF()轉化為擁有引用。

從Python調用的C函數返回的對象引用必須是擁有引用 - 擁有權將從函數傳遞給調用者。

雷區

有幾種情況看似無害地使用借用引用會導致問題,這些都與解釋器的隱式調用有關,這可能導致引用的所有者釋放它。

要了解的第一個也是最重要的案例是在不相關對象上使用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],然後用0替換list[1]的值,最後打印借用的引用。看起來沒什麽問題,對吧?但實際上不是!

讓我們按照控制流程進入PyList_SetItem()。該列表擁有對其所有項目的引用,因此當項目1被替換時,它必須處理原始項目1。現在讓我們假設原始項目1是用戶定義的類的一個實例,並且讓我們進一步假設類定義了一種 del()方法。如果此類實例的引用計數為1,則處置它將調用其__del__()方法。

由於它是用Python編寫的,所以該__del__()方法可以執行任意的Python代碼。它也執行了一些導致item引用無效的bug()函數?假設傳入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! */
}

空指針

一般來說,將對象引用作為參數的函數並不期望你傳遞NULL指針給它們,並且如果你這樣做的話,將會導致崩潰。返回對象引用的函數通常僅當發生異常才返回NULL,不測試NULL參數的原因是函數通常會將它們接收到的對象傳遞給其他函數 - 如果每個函數都要測試NULL,則會有大量冗余測試,並且代碼運行速度會更慢。

當收到一個可能為NULL的指針時,最好僅在最開始處測試NULL,例如在malloc()或可能引發異常的函數返回處測試。

宏Py_INCREF()和Py_DECREF()不檢查NULL指針-但是,它們的變體Py_XINCREF()和Py_XDECREF()會檢查。

用於檢查特定對象類型的宏(Pytype_Check())不會檢查NULL指針 - 因為有很多代碼在針對各種不同的預期類型時,會連續調用幾個宏來測試一個對象,如果檢查NULL指針的話會產生冗余測試,所以對於檢查特定對象類型的宏沒有帶NULL檢查的變體。

C函數調用機制保證傳遞給C函數的參數列表(args在這個例子中)永遠不是NULL - 事實上它保證它總是一個元組。

讓一個NULL指針"逃到"Python用戶這是一個嚴重的錯誤。

在C++中編寫擴展

可以用C++編寫擴展模塊,但有些限制。如果主程序(Python解釋器)由C編譯器編譯和鏈接,則不能使用帶構造函數的全局對象或靜態對象。如果主程序由C++編譯器鏈接就沒有問題。Python解釋器調用的函數(特別是模塊初始化函數)必須使用extern "C"聲明。沒有必要將Python頭文件包含在extern "C" {...} - 如果定義了符號__cplusplus,它們就會使用這種形式(所有最新的C++編譯器都定義了此符號)。

為擴展模塊提供C

許多擴展模塊只是提供了Python中使用的新功能和類型,但有時擴展模塊中的代碼可能對其他擴展模塊有用。例如擴展模塊可以實現類似"收集"的類型,其工作方式類似於沒有順序的列表。就像標準的Python列表類型有一個允許擴展模塊創建和操作列表的C API一樣,這個新的集合類型應該有一組C函數用於直接從其他擴展模塊進行操作。

乍一看,這看起來很簡單:只需編寫函數(沒有static聲明),提供適當的頭文件,並添加C API文檔。事實上如果所有的擴展模塊總是與Python解釋器靜態鏈接的話是沒問題的,但是當模塊用作共享庫時,一個模塊中定義的符號可能對另一個模塊不可見,可見性的細節取決於操作系統,一些系統為Python解釋器和所有擴展模塊(例如Windows)使用一個全局名稱空間,而其他系統則需要在模塊鏈接時間(例如AIX)顯式導入導入的符號列表或者提供不同策略的選擇(大多數Unix系統),即使符號是全局可見的,其希望調用的函數的模塊可能尚未加載!

因此可移植性不需要對符號可見性做出任何假設,這意味著除了模塊的初始化函數之外,擴展模塊中的所有符號都應聲明為static,以避免與其他擴展模塊發生名稱沖突(如The Module’s Method Table and Initialization Function所述),這意味著從其他擴展模塊訪問的符號必須以不同的方式導出。

Python提供了一種特殊的機制來將C級信息(指針)從一個擴展模塊傳遞到另一個擴展模塊:Capsules。Capsule是一個存儲void *指針的Python數據類型。Capsule只能通過C API創建和訪問,但它們可以像任何其他Python對象一樣傳遞。特別的是可以分配擴展模塊名稱空間中的名稱給它們,然後其他擴展模塊可以導入該模塊,檢索該名稱的值,然後從Capsule檢索指針。

Capsules有許多方式導出擴展模塊的C API,每個函數都可以獲得自己的Capsule或者所有C API指針都可以存儲在一個地址在Capsule中發布的數組中。存儲和檢索指針的各種任務可以在提供代碼的模塊和客戶模塊之間以不同的方式分配。

無論您選擇哪種方法,正確命名Capsules非常重要。函數PyCapsule_New()接受一個const char *類型的名稱參數,您可以傳入一個NULL,但我們強烈建議您指定一個名稱。

特別是用於公開C API的Capsules應按照以下約定命名:

modulename.attributename

PyCapsule_Import()函數可以非常方便的加載Capsule提供的C API,但前提是Capsule的名稱符合此慣例,這種行為使得C API用戶可以高度肯定他們加載的Capsule包含正確的C API。

以下示例演示了一種將導出模塊的寫入程序的大部分負擔放在常用庫模塊的適當位置的方法。它將所有C API指針(示例中只有一個!)存儲在一個void指針數組中,該數組成為Capsule的值。與模塊相對應的頭文件提供了一個宏,它負責導入模塊並檢索其C API指針,客戶端模塊只需在訪問C API之前調用此宏。

導出模塊是對A Simple Example模塊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 (c_api_object != NULL)
        PyModule_AddObject(m, "_C_API", c_api_object);
    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) */

為了訪問函數PySpam_System(),客戶端模塊必須在其初始化函數中調用函數(或者說宏)import_spam():

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相當復雜,但是對於每個導出的函數,其基本結構都是相同的,因此僅需要學習一次。

(一)用C或C ++擴展(翻譯)