1. 程式人生 > >淺談Python C擴充套件

淺談Python C擴充套件


很多時候,我們需要寫Python的C擴充套件,例如為了提高速度,用一些C的庫等等。本文首先整理了python呼叫C擴充套件以及在C中呼叫python的方法;然後重點分析了CPython API中的引用計數問題。

在python應用中,為了對效能進行優化,我們常常需要寫python的C擴充套件,將一些關鍵程式碼用C進行重寫以提高效能;同時,我們也可以用在C中呼叫python的方法,例如寫回調函式等。不管是python呼叫C,還是C呼叫python,最重要的是引用計數的管理,這也是最容易引起問題的地方。本文首先從簡單的範例開始講解python和C的互相呼叫,然後重點學習CPython API的引用計數問題。對python C擴充套件比較熟的可以直接跳過前面兩部分,只看第三部分(大神請忽視本文)。

1. Python C 擴充套件基礎

1.1 主要步驟

首先,我們看看用C寫一個python擴充套件需要哪些步驟:

  • 包含標頭檔案Python.h
  • 你需要作為python介面的C函式
  • 一個將你的函式對映為python介面的對映表
  • 一個初始化函式

1.1.1 Python.h標頭檔案

這個標頭檔案包含了所有的用來將你的模組hook到python解析器的CPython API,而且你必須將這個標頭檔案寫在任何標準標頭檔案之前,這是因為這個標頭檔案可能定義了一些影響標準標頭檔案的預處理巨集。

1.1.2 C函式

python C 擴充套件的函式定義一般是下面的三種形式之一:

static PyObject *MyFunction( PyObject *self, PyObject *args );

static PyObject *MyFunctionWithKeywords(PyObject *self,  PyObject *args, PyObject *kw);

static PyObject *MyFunctionWithNoArgs( PyObject *self );

Python中的函式都返回PyObject型別的指標,沒有像C那種返回void型別的;如果你的函式不想返回一個值的話,Python定義了一個巨集Py_RETURN_NONE

,它等價於在指令碼層返回None。
你的C函式應該是個靜態函式,名字是任意的,但一般命名為模組名_函式名的形式,所以,一個典型的函式長這樣:

static PyObject *modulename_func(PyObject *self, PyObject *args) {
   /* Do something here. */
   Py_RETURN_NONE;
}

1.1.3 方法對映表

方法對映表就是PyMethodDef結構的陣列,而PyMethodDef結構體長這樣:

struct PyMethodDef {
   char *ml_name;
   PyCFunction ml_meth;
   int ml_flags;
   char *ml_doc;
};

其各個引數的意義如下:

  • ml_name: 這是暴露給python程式的函式名;
  • ml_meth: 這是指向1.1.2所講的函式的指標,也就是真正函式定義的地方;
  • ml_flags: 這告訴python解析器想用三種函式簽名的哪一種,一般來說,它的值是METH_VARARGS;如果你想傳入關鍵字引數的話,也可以與MET_KEYWORDS進行或運算;當然,如果你不想接受任何引數的話,可以給其賦值為METH_NOARGS;
  • ml_doc: 這是函式的文件字串,如果你不想寫的話,直接給其賦值為NULL。

最後要注意的是,這個對映表應該以一個由NULL和0組成的結構體進行結尾。所以,一個方法對映表應該長這樣:

static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_VARARGS, NULL },
   { NULL, NULL, 0, NULL }
};

1.1.4 初始化函式

你的擴充套件模組的最後一部分就是初始化函數了,它會在模組被匯入時被python解析器呼叫。初始化函式必須被命名為initModuleName,這裡ModuleName表示你的模組名。
這個初始化函式需要從你構建的庫中匯出,所以Python標頭檔案裡定義了PyMODINIT_FUNC來進行這項工作,你需要做的就是在定義函式時使用它;這個函式也應該是你的模組中唯一一個非static的項。這個初始化函式的原型一般是這樣的:

PyMODINIT_FUNC initModuleName() {
   Py_InitModule3(ModuleName, module_methods, "docstring...");
}

py_InitModule3的引數定義如下:

  • module_name: 被匯出的模組名;
  • module_methods: 上面所定義的對映表;
  • docstring: 你想要給你的模組的註釋;

將上面的所有步驟結合在一起,一個C擴充套件模組看起來長這樣:

#include <Python.h>

static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Do your stuff here. */
   Py_RETURN_NONE;
}

static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_VARARGS, NULL },
   { NULL, NULL, 0, NULL }
};

PyMODINIT_FUNC initModule() {
   Py_InitModule3(Module, module_methods, "docstring...");
}

1.2 Example

在1.1節我們已經覆蓋了一個簡單C擴充套件模組所需的所有知識點,現在我們通過一個例項來實踐下;我們的C模組實現的功能是兩個浮點數的乘法和除法,最後編譯成名為example的模組。
首先,根據上面的知識點,我們寫一個example.c原始檔,內容如下:

#include <Python.h>

static PyObject* example_mul(PyObject* self, PyObject*args)
{
    float a, b;
    if(!PyArg_ParseTuple(args, "ff", &a, &b))
    {
        return NULL;
    }
    return Py_BuildValue("f", a*b);
}

static PyObject* example_div(PyObject* self, PyObject*args)
{
    float a, b;
    if(!PyArg_ParseTuple(args, "ff", &a, &b))
    {
        return NULL;
    }
    return Py_BuildValue("f", a/b);  // to deal with b == 0
}

static char mul_docs[] = "mul(a, b): return a*b\n";
static char div_docs[] = "div(a, b): return a/b\n";

static PyMethodDef example_methods[] =
{
    {"mul", (PyCFunction)example_mul, METH_VARARGS, mul_docs},
    {"div", (PyCFunction)example_div, METH_VARARGS, div_docs},
    {NULL, NULL, 0, NULL}
};

void PyMODINIT_FUNC initexample(void)
{
    Py_InitModule3("example", example_methods, "Extension module example!");
}

這裡PyArg_ParseTuple和Py_BuildValue分別用來解析python的引數和構建python的值,這兩個函式將在下面講到,這裡需要注意的是因為我們要匯出example這個模組,所以最後的initModuleName的ModuleName以及呼叫的Py_InitModule3的第一個引數的名字都是example.

1.2.1 編譯和安裝擴充套件

有了這個原始檔,我們應該怎麼編譯和安裝這個擴充套件,使得它成為我們可以匯入的python模組的一部分呢?答案是distutils模組,它就是用來發布python模組的(官方推薦使用setuptools,但我沒有去研究怎麼用).
我們首先定義個setup.py指令碼檔案,內容如下:

from distutils.core import setup, Extension
setup(name="exampleAPP", version="1.0", ext_modules=[Extension("example", ["example.c"])])

這裡需要注意的是ext_modules裡的Extension的模組名必須和我們想要匯出的模組名相同(這裡就是exmaple),否則會出現LINK : error LNK2001: unresolved external symbol的錯誤,然後我們用下面這個命令進行編譯與安裝:

python setup.py install

安裝成功後,就會在python_path/Lib/site-packages下面生成example.pyd這個模組和exampleAPP-1.0-py2.7.egg-info這個檔案,就可以匯入和使用了:

result

注意:在windows下,使用vs進行編譯的的話,可能會出錯:error: Unable to find vcvarsall.bat
在StackOverflow上找到了答案:error: Unable to find vcvarsall.bat,原因是當用setup.py去安裝包時,python 2.7會尋找 Visual Studio 2008(python 2.7就是用VS2008編譯的),找不到的話就會報這個錯;一種trick的方法是根據你安裝的VS版本,在執行setup.py之前先執行以下命令:

Visual Studio 2010 (VS10): SET VS90COMNTOOLS=%VS100COMNTOOLS%  
Visual Studio 2012 (VS11): SET VS90COMNTOOLS=%VS110COMNTOOLS%
Visual Studio 2013 (VS12): SET VS90COMNTOOLS=%VS120COMNTOOLS%
Visual Studio 2015 (VS14): SET VS90COMNTOOLS=%VS140COMNTOOLS%

但這種做法並不保險,而且用與編譯python本身不同版本的編譯器去編譯python C擴充套件還可能引起不相容問題,正確的做法是下載Visual C++ 2008或者 Microsoft Visuial C++ Compiler for Python(需要setuptools和wheel這兩個python包,而且必須要用setuptools.setup()而不是distutils來進行安裝。)

1.3 引數提取——PyArg_ParseTuple函式

上面的例子中,指令碼層傳入的引數會存在PyObject* args所指向的PyObject裡面,那麼我們怎麼提取出引數呢?答案是使用PyArg_ParseTuple函式,它的原型是這樣的:

int PyArg_ParseTuple(PyObject* tuple,char* format,...)

這個函式遇到錯誤返回0,返回別的數字代表正確。tuple就是C函式傳進來的第二個引數,format是描述引數格式的字串,裡面的格式碼意義如下:

Code C type Meaning
c char A Python string of length 1 becomes a C char
d double A Python float becomes a C double
f float A Python float becomes a C float
i int A Python int becomes a C int
l long A Python int becomes a C long.
L long long A Python int becomes a C long long
O PyObject* Gets non-NULL borrowed reference to Python argument.
s char* Python string without embedded nulls to C char*.
s# char*+int Any Python string to C address and length.
t# char*+int Read-only single-segment buffer to C address and length.
u Py_UNICODE* Python Unicode without embedded nulls to C.
u# Py_UNICODE*+int Any Python Unicode C address and length.
w# char*+int Read/write single-segment buffer to C address and length.
z char* Like s, also accepts None (sets C char* to NULL).
z# char*+int Like s#, also accepts None (sets C char* to NULL).
(…) as per … A Python sequence is treated as one argument per item.
| The following arguments are optional.
: Format end, followed by function name for error messages.
; Format end, followed by entire error message text.

剩餘的引數就是變數的地址,而變數的型別由格式串的格式碼決定。要解析帶有關鍵字的引數的話,請使用PyArg_ParseTupleAndKeywords

int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)

1.4 返回值和Py_BuildValue

Python C 函式的返回值都是PyObject*型別的(錯誤返回NULL),如果不想返回任何值,就是用巨集Py_RETURN_NONE。Py_BuildValue剛好和PyArg_ParseTuple相反,它是用來將C的變數構建為Python的PyObject*的(但這時傳入的不是地址,而是值),它的原型如下:

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

這個字串格式碼和上面的類似,下面列出了常用的位元組碼:

Code C type Meaning
c char A C char becomes a Python string of length 1.
d double A C double becomes a Python float.
f float A C float becomes a Python float.
i int A C int becomes a Python int.
l long A C long becomes a Python int.
N PyObject* Passes a Python object and steals a reference.
O PyObject* Passes a Python object and INCREFs it as normal.
O& convert+void* Arbitrary conversion
s char* C 0-terminated char* to Python string, or NULL to None.
s# char*+int C char* and length to Python string, or NULL to None.
u Py_UNICODE* C-wide, null-terminated string to Python Unicode, or NULL to None.
u# Py_UNICODE*+int C-wide string and length to Python Unicode, or NULL to None.
w# char*+int Read/write single-segment buffer to C address and length.
z char* Like s, also accepts None (sets C char* to NULL).
z# char*+int Like s#, also accepts None (sets C char* to NULL).
(…) as per … Builds Python tuple from C values.
[…] as per … Builds Python list from C values.
{…} as per … Builds Python dictionary from C values, alternating keys and values.

{…} 用來從偶數個key和value隔開的C的值中構建字典,例如Py_BuildValue("{issi}", 23, "zig", "zag", 42)返回一個python的字典:{23:’zig’, ‘zag’:42}.

1.5 錯誤和異常處理

當一個函式失敗時,Python直譯器的一個重要約定是返回一個錯誤值(一般是NULL)並設定3個全域性靜態變數,分別對應Python的sys.exec_type, sys.exec_value和sys.exec_traceback. 最先檢測到異常的函式應該報告並設定全域性變數,其它呼叫它的函式應該只是返回異常值,例如:當f呼叫g並檢測到g失敗了,它應該返回一個錯誤值(一般是NULL或-1),它不應該呼叫任何一個PyErr_*()函式,這應該是g呼叫的。f的呼叫者也應該返回一個錯誤值,以此類推。
python API定義了一些函式來設定並檢查各種異常:
(1)PyErr_SetString(PyObject* type, const char* message):
type一般是一個預定義的物件,例如PyExc_ZeroDivisionError,C字串用來說明異常出現的原因
(2)PyErr_SetObject(PyObject* type, PyObject* value):
最常用
(3)PyErr_Occurred():
用來檢查是否設定了一個異常
(4)如果想要忽視一個異常而不傳遞給解析器的話,可以呼叫PyErr_Clear()函式
(5)所有直接呼叫malloc()或者realloc()的函式失敗的話,必須要呼叫PyErr_NoMemory(),並且返回失敗標誌

1.6 小結

本節講解了寫一個C模組的一些基本知識點和約定的異常處理流程,並用一個例項展示瞭如何編譯與呼叫C模組,下一節我們講下如何從C中呼叫python的方法。

2. C呼叫Python

C呼叫Python的方法也很簡單,下面我們以windows+VS2015+python2.7講解下如何用C呼叫Python.
首先,我們新建一個工程,並將python的包含目錄和庫目錄設定到工程的目錄裡面去(注意,這裡要設定release版本的,因為我們下載的python是release版本的,如果用debug的話,會在編譯時出現Error: cannot open file ‘python27_d.lib’錯誤),如下圖所示:

set up

然後,我們新建原始檔,內容如下所示:

#include <Python.h>

int main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pDict, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    if (argc < 3) {
        fprintf(stderr, "Usage: call pythonfile funcname [args]\n");
        return 1;
    }

    Py_Initialize();        // Initialize the Python Interpreter
    pName = PyString_FromString(argv[1]);    // Build the name object

    pModule = PyImport_Import(pName);
    Py_DECREF(pName);

    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */

        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyInt_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyInt_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr, "Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    Py_Finalize();     // Finish the Python Interpreter
    return 0;
}

我們在工程目錄下新建Mul.py,內容如下:

def multiply(a,b):
    print "Will compute", a, "times", b
    c = 0
    for i in range(0, a):
        c = c + b
    return c

執行,得到結果:

Will compute 3 times 4
Result of call: 12
請按任意鍵繼續. . .

C呼叫Python的原始碼還是很直觀的,其中最難的部分是那些Py_DECREF()和Py_XDECREF(),這是什麼?第一次看確實會一頭霧水,別急,下面一節我們就要講python C API的引用計數。

3. Reference Counts

在使用Python C API時,最容易出錯的地方就是引用計數的管理。不管是記憶體洩露還是非法記憶體放訪問,對於程式來說都是致命的,下面我們就簡單講講CPython API中的引用計數。

3.1 CPython引用計數簡介

在C/C++中,程式設計師負責動態記憶體的申請與釋放釋放,在C中,這是通過呼叫malloc()/free()來實現的;如果只進行了記憶體申請而沒有手動釋放就會造成記憶體洩露,而如果使用已釋放的記憶體就會造成非法記憶體訪問(use freed memory);由於CPython大量使用malloc()free(),所以需要一種策略來避免記憶體洩露和非法記憶體訪問,CPython是通過使用引用計數(reference counting)來實現的。
CPython具有兩個巨集Py_INCREF(x)Py_DECREF(x)(Py_XINCREFPy_XDECREF的作用和它們類似,只是會檢查傳進去的指標是否為空),分別用來增加和減少引用計數,此外,Py_DECREF也會在引用計數減少到0後釋放物件;那麼問題來了,什麼時候使用Py_INCREF(x)Py_DECREF(x)呢?
要回答前面的這個問題,我們要首先引入CPython的一些術語。在CPython中,沒有人擁有一個物件,擁有的是物件的引用;引用的擁有者負責在引用不再引用這個物件時對它呼叫Py_DECREF,引用的擁有權也可以轉移。在CPython中,使用術語”New”,”Stolen”和”Borrowed” references來表示三種引用,這些術語其實是表明誰是引用的真正擁有者,即誰負責對引用進行處理。

  • New References:
    當新建一個PyObject物件時,就產了一個New Reference,例如當呼叫PyInt_FromLong時。New Reference意味著你擁有這個引用。
  • Stolen References:
    這一般出現在函式呼叫時將一個引用傳進去當引數時,這個函式會假設現在它擁有這個引用,即它會“偷取”這個引用,這意味著當你呼叫這個函式後,你就不再擁有這個引數的引用。例如當呼叫PyList_SetItem(PyObject* list, index, PyObject* item)後,你就不再擁有對item的引用
  • Borrowed References:
    Borrowed Reference一般出現在檢視一個PyObject時,例如從一個列表裡面獲取一個成員。借來的引用不應該呼叫Py_DECREF,而且它持有物件的時間不應該比引用的擁有者長,如果在引用的擁有者已經釋放這個引用後,還是訪問借來的引用,就會造成非法記憶體訪問;借來的引用也可以通過呼叫Py_INCREF變為擁有的引用。

3.2 CPython 引用擁有權規則

3.2.1 擁有權規則簡單概括

在3.1節我們簡單介紹了Cpython的引用計數,現在我們概括下引用的擁有權的規則,主要分為呼叫函式時作為引數傳入的引用擁有權轉移規則和作為函式返回值的引用的擁有權的轉移規則:

  • 作為函式返回值時:
    (1)大部分返回引用的函式都會將這個引用的擁有權轉移到函式呼叫者(即返回新的引用),例如PyInt_FromLongPy_BuildValue等;
    (2)然而也有少數例外,例如PyTuple_GetItem(),PyList_GetItem(),PyDIct_GetItem()PyDict_GetItemString(),它們返回的是borrowed ReferencePyImport_AddModule()返回的也是借來的引用。
  • 作為引數傳遞時:
    (1)在你將一個物件的引用傳遞進另一個函式時,一般來說這個函式會從你借這個引用,也就是說,在函式,一般引數的引用是borrowed reference;
    (2)有兩個比較重要的例外,PyTuple_SetItem()PyList_SetItem(),它們會從你這偷取引用(steal reference),這意味著當你把引用傳遞給這些函式時,這些函式就會擁有這些引用,而你不再擁有這些引用。

3.2.2 引用擁有權例外總結

就像上節總結的一樣,我們只要記住一般來說,作為返回值的引用是一個新的引用,我們要負責其釋放;而作為引數傳入的引用一般是borrowed reference,我們用完就可以了;而那些例外的函式總結如下:

  • 從引數中steal reference的:
PyCell_SET (but not PyCell_Set)
PyList_SetItem
PyList_SET_ITEM
PyModule_AddObject
PyTuple_SetItem, PyTuple_SET_ITEM
  • 返回borrowed reference的函式
all PyArg_Xxx functions
PyCell_GET (but not PyCell_Get)
PyDict_GetItem
PyDict_GetItemString
PyDict_Next
PyErr_Occurred
PyEval_GetBuiltins
PyEval_GetFrame
PyEval_GetGlobals
PyEval_GetLocals
PyFile_Name
PyFunction_GetClosure
PyFunction_GetCode
PyFunction_GetDefaults
PyFunction_GetGlobals
PyFunction_GetModule
PyImport_AddModule
PyImport_GetModuleDict
PyList_GetItem, PyList_GETITEM
PyMethod_Class, PyMethod_GET_CLASS
PyMethod_Function, PyMethod_GET_FUNCTION
PyMethod_Self, PyMethod_GET_SELF
PyModule_GetDict
PyObject_Init
PyObject_InitVar
PySequence_Fast_GET_ITEM
PySys_GetObject
PyThreadState_GetDict
PyTuple_GetItem, PyTuple_GET_ITEM
PyWeakref_GetObject, PyWeakref_GET_OBJECT
Py_InitModule
Py_InitModule3
Py_InitModule4

3.3 關於引用的易錯點

上面兩節我們介紹了引用以及引用的擁有權規則,現在我們講講CPython中引用中容易犯的錯誤,引用主要容易出兩類錯誤:
(1)引用不再指向物件後沒有減少引用計數導致記憶體洩露,類似於在C中呼叫了malloc()而沒有呼叫free(),例如:

static PyObject *bad_incref(PyObject *pObj) {
    Py_INCREF(pObj);
    /* ... a metric ton of code here ... */
    if (error) {
        /* No matching Py_DECREF, pObj is leaked. */
        return NULL;
    }
    /* ... more code here ... */
    Py_DECREF(pObj);
    Py_RETURN_NONE;
}

(2)在物件釋放後仍然通過引用去訪問物件,類似於在C中free()以後去獲取物件或者使用野指標(dangling pointer),例如:

static PyObject *bad_incref(PyObject *pObj) {
    /* Forgotten Py_INCREF(pObj); here... */

    /* Use pObj... */

    Py_DECREF(pObj); /* Might make reference count zero. */
    Py_RETURN_NONE;  /* On return caller might find their object free'd. */
}

函式返回後,呼叫者可能會使用已經釋放掉的pObj,這是一個典型的access-after-free錯誤。
上面舉例所示的錯誤都是小心點就可以避免的,然而有些引用錯誤就比較隱蔽,也是我們需要特別注意的地方,下面我們通過舉例來進行說明。

3.3.1 New References比較容易出現的錯誤

對於New Reference,我們最容易犯的錯誤就是將一個函式返回的New Reference作為臨時變數傳進函式的引數,由於大部分函式的引數傳遞都是以Borrowed Reference進行的,就會導致這個New Reference沒有人對其進行引用計數管理,從而導致記憶體洩露。以下的函式是將兩個數進行詳見,我們用第一節的方法將其編譯成python的擴充套件模組,並將example_substract匯出為sub介面進行呼叫。

static PyObject* subtract_long(long a, long b) {
    PyObject *pA, *pB, *r;

    pA = PyLong_FromLong(a);        /* pA: New reference. */
    pB = PyLong_FromLong(b);        /* pB: New reference. */
    r = PyNumber_Subtract(pA, pB);  /*  r: New reference. */
    Py_DECREF(pA);                  /* My responsibility to decref. */
    Py_DECREF(pB);                  /* My responsibility to decref. */
    return r;                       /* Callers responsibility to decref. */
}

static PyObject* example_subtract(PyObject* self, PyObject* args)
{
    PyObject* result;
    long a, b;
    if(!PyArg_ParseTuple(args, "ll", &a, &b))
    {
        return NULL;
    }
    result = subtract_long(a, b);
    return result;
}

然而,一個很容易犯的錯誤就是在呼叫PyNumber_Subtrace時,我們直接將PyLong_FromLong(x)傳進去,由於PyNumber_Substract()只會借取引用,它並不會釋放引用,這時返回的New Reference並沒有對其進行Py_DECREF,就會導致記憶體洩露,如下example_bad_subtrace,我們將其匯出為bad_sub介面:

static PyObject* bad_subtract_long(long a, long b) {
    PyObject *r;
    r = PyNumber_Subtract(PyLong_FromLong(a), PyLong_FromLong(b));  /*  r: New reference. */
    return r;                       /* Callers responsibility to decref. */
}

static PyObject* example_bad_subtract(PyObject* self, PyObject* args)
{
    PyObject* result;
    long a, b;
    if(!PyArg_ParseTuple(args, "ll", &a, &b))
    {
        return NULL;
    }
    result = bad_subtract_long(a, b);
    return result;
}

ipython_memory_usage對記憶體進行測量,分別呼叫example.subexample.bad_sub,看是否有記憶體洩露:
memory usage

從結果可以看到,每呼叫100000次左右的example.bad_sub,就會導致3M左右的記憶體洩露,從而印證了我們的猜想。

3.3.2 Stolen References比較容易出現的錯誤

CPython中Stolen Reference的情況不多,兩個最重要的需要記住的就是PyList_SetItemPyTuple_SetItem,對於Stolen Reference,我們只需要記住當引用傳進這兩個函式後,我們便不再擁有對引用的擁有權,也就不能再對其進行Py_DECREF了。

static PyObject *make_tuple(void) {
    PyObject *r;
    PyObject *v;

    r = PyTuple_New(3);         /* New reference. */
    v = PyLong_FromLong(1L);    /* New reference. */
    /* PyTuple_SetItem "steals" the new reference v. */
    PyTuple_SetItem(r, 0, v);
    /* This is fine. */
    v = PyLong_FromLong(2L);
    PyTuple_SetItem(r, 1, v);
    Py_DECREF(v);    /* Now we are interfering with r's internals. */
    /* More common pattern. */
    PyTuple_SetItem(r, 2, PyUnicode_FromString("three"));
    return r; /* Callers responsibility to decref. */
}

當v被傳遞給PyTuple_SetItem後,v的引用被偷走了,它成為了一個borrowed reference, 再對它呼叫Py_DECREF可能會引起未知的行為。

3.3.3 Borrowed References比較容易出現的錯誤

在引用出現錯誤的地方,最奇怪的bug常常和borrowed reference有關。
例如我們用borrowed reference來操作列表的最後一個元素,操作步驟如下:
* 從列表中得到最後一個元素的borrowed reference
* 對列表進行操作do_something()
* 操作最後一個元素的borrowed reference,這裡只是簡單的列印它。
程式碼如下:

static PyObject *pop_and_print_BAD(PyObject *pList) {
    PyObject *pLast;

    pLast = PyList_GetItem(pList, PyList_Size(pList) - 1);
    fprintf(stdout, "Ref count was: %zd\n", pLast->ob_refcnt);
    do_something(pList);
    fprintf(stdout, "Ref count now: %zd\n", pLast->ob_refcnt);
    PyObject_Print(pLast, stdout, 0);
    fprintf(stdout, "\n");
    Py_RETURN_NONE;
}

這裡PLast是一個borrowed reference,這段程式碼看起來似乎沒有問題,但讓我們再仔細分析,pList擁有對它的物件的所有引用,所以在do_something中可能釋放任何元素的引用,當它釋放了所有元素的引用後,PLast是否還有效取決於最後一個元素是否還有別的引用。例如do_something可能如下:

void do_something(PyObject *pList) {
    while (PyList_Size(pList) > 0) {
        PySequence_DelItem(pList, 0);
    }
}

那麼,呼叫這個函式會發生什麼事情?下面是一些例子(pop_and_pring_BAD被對映為cPyRefs.popBAD):
(1) 呼叫如下程式碼時,引用計數完全錯誤了,但是由於記憶體沒有被改寫,所以列印最後一個元素貌似是正確的。

>>> l = ["Hello", "World"]
>>> cPyRefs.popBAD(l)       # l will become empty
Ref count was: 1
Ref count now: 4302027608
'World'

(2) 以下程式碼出現了段錯誤,這個錯誤就比較明顯了。

>>> l = ['abc' * 200]
>>> cPyRefs.popBAD(l)
Ref count was: 1
Ref count now: 2305843009213693952
Segmentation fault: 11

(3) 當呼叫下面的程式碼時,問題似乎又不見了,因為最後一個元素有額外的引用。

>>> l = ["Hello", "World"]
>>> a = l[-1]
>>> cPyRefs.popBAD(l)
Ref count was: 2
Ref count now: 1
'World'

上面這個例子的錯誤很難被發現,因為這個C函式的正確性依賴於呼叫者是否擁有額外的引用以及do_something的操作。當然,我們知道了引起問題的原因,解決方案也很簡單,用borrowed references時,如果你對物件感興趣,你就應該為引用計數加1,然後在不用的時候再減1

static PyObject *pop_and_print_BAD(PyObject *pList) {
    PyObject *pLast;

    pLast = PyList_GetItem(pList, PyList_Size(pList) - 1);
    Py_INCREF(pLast);       /* Prevent pLast being deallocated. */
    /* ... */
    do_something(pList);
    /* ... */
    Py_DECREF(pLast);       /* No longer interested in pLast, it might     */
    pLast = NULL;           /* get deallocated here but we shouldn't care. */
    /* ... */
    Py_RETURN_NONE;
}

總結

在本文中,我們首先在第一節和第二節簡單介紹了寫Python C 擴充套件的方法和C呼叫Python的方法,然後在第三節,我們重點介紹了CPython API中的引用計數,以及引用計數中容易出現的記憶體洩露和非法記憶體訪問問題,總的來說,幾個比價重要的結論如下:

  • 大部分返回引用的函式都會將這個引用的擁有權轉移到函式呼叫者,但PyTuple_GetItem(),PyList_GetItem(),PyDIct_GetItem()PyDict_GetItemString()返回的是borrowed Reference
  • 在你將一個物件的引用傳遞進另一個函式時,一般來說這個函式會從你借這個引用,但PyTuple_SetItem()PyList_SetItem()們會從你這偷取引用(steal reference);
  • 不要將返回New Reference的函式呼叫作為臨時變數傳遞給一個函式的形參,例如PyNumber_Subtract(PyLong_FromLong(a), PyLong_FromLong(b)),會引起記憶體洩露;
  • 用borrowed references時,如果你對物件感興趣,你就應該為引用計數加1,然後在不用的時候再減1

參考文獻