承接上文,發現在使用Python C/C++ API擴展Python模塊時,總要在各種各樣的地方考慮到引用計數問題,稍不留神可能會導致擴展的模塊存在記憶體泄漏。引用計數問題是C語言擴展Python模塊最頭疼的地方,需要由程式員對使用的每個C API都要充分瞭解,甚至要熟悉源碼才能精確掌握什麼時候引用計 ...
承接上文,發現在使用Python C/C++ API擴展Python模塊時,總要在各種各樣的地方考慮到引用計數問題,稍不留神可能會導致擴展的模塊存在記憶體泄漏。引用計數問題是C語言擴展Python模塊最頭疼的地方,需要由程式員對使用的每個C API都要充分瞭解,甚至要熟悉源碼才能精確掌握什麼時候引用計數加一,什麼時候減一。
本文為翻譯文章,我覺得對於源碼中的引用計數講解得比較清楚,所以就翻譯為中文。http://edcjones.tripod.com/refcount.html#
Summary:
Python Object的結構體定義包含一個引用計數和對象類型:
#define PyObject_HEAD \ int ob_refcnt; \ struct _typeobject *ob_type; typedef struct _object { PyObject_HEAD } PyObject;
Python提供了兩組與引用計數相關的巨集定義【object.h】:
#define Py_INCREF(op) ( \ _Py_CHECK_THREAD_SAVE \ _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \ ((PyObject*)(op))->ob_refcnt++)
/*當引用計數為0時,會釋放對象所占的記憶體*/ #define Py_DECREF(op) \ do { \ if (_Py_CHECK_THREAD_SAVE \ _Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \ --((PyObject*)(op))->ob_refcnt != 0) \ _Py_CHECK_REFCNT(op) \ else \ _Py_Dealloc((PyObject *)(op)); \ } while (0)
另外一組考慮是對象為NULl的情況:
#define Py_XINCREF(op) do { if ((op) == NULL) ; else Py_INCREF(op); } while (0) #define Py_XDECREF(op) do { if ((op) == NULL) ; else Py_DECREF(op); } while (0)
在Python中,沒有誰能真正擁有一個對象,只擁有對象的引用。一個對象的reference count定義為該對象的引用者數量,對象的引用者當不再使用該對象時有責任主動調用Py_DECREF(),當reference count為0時,Python可能會delete這個對象。
每次調用Py_INCREF(),最終都應該對應調用Py_DECREF()。C語言中,每個malloc,必須最終調用free()。而現實很容易忘記free掉在堆上分配的記憶體,而且不使用工具的話也難以察覺記憶體泄漏問題,因為現代機器記憶體、虛擬記憶體都很充足,一般會在長時間運行的伺服器程式上出現記憶體泄漏問題。
當一個指向Python Object的指針的引用計數加1,就是說這個對象被protected.
什麼時候調用Py_INCREF()和Py_DECREF()
從 函數中返回一個Python Object
大部分Python 對象是通過Python C API提供的函數來創建的,一般形如 PyObject* Py_Something(arguments)創建一個Python 對象,然後返回給調用者。一般在Py_Something函數中對該Python對象調用了Py_INCREF(並不是所有的函數都會調用),而調用Py_Something的函數在使用其返回的Python對象時要牢記該對象引用計數已被加1,當不再需要該對象時需要調用Py_DECREF()。
void MyCode(arguments) { PyObject* pyo; ... pyo = Py_Something(args);
MyCode函數調用了Py_Something,有責任處理pyo的引用計數,當MyCode使用完了pyo之後,必須要調用Py_DECREF(pyo)。
不過,如果MyCode需要返回pyo對象,比如:
PyObject* MyCode(arguments) { PyObject* pyo; ... pyo = Py_Something(args); ... return pyo; }
此時,MyCode不應該調用PY_DECREF(),在這種情況下,MyCode將pyo對象的引用計數責任傳遞了出去。
Note:如果一個函數返回的是None對象,C代碼應該是這樣:必須要增加None對象的引用計數。
Py_INCREF(Py_None); return Py_None;
到目前為止討論了最常見的情況,即當調用Py_Something創建了一個引用,並將引用計數的責任傳遞給其調用者。在Python文檔中,這被稱為new reference。比如文檔中有說明:
PyObject* PyList_New(int len) Return value: New reference. Returns a new list of length len on success, or NULL on failure.
當一個引用被INCREF,通常稱為這個引用被protected。
有時候,Python源碼中不會調用Py_DECREF()。
PyObject * PyTuple_GetItem(register PyObject *op, register int i) { if (!PyTuple_Check(op)) { PyErr_BadInternalCall(); return NULL; } if (i < 0 || i >= ((PyTupleObject *)op) -> ob_size) { PyErr_SetString(PyExc_IndexError, "tuple index out of range"); return NULL; } return ((PyTupleObject *)op) -> ob_item[i]; }
這種情況被稱為borrowing a reference。
PyObject* PyTuple_GetItem(PyObject *p, int pos) Return value: Borrowed reference. Returns the object at position pos in the tuple pointed to by p. If pos is out of bounds, returns NULL and sets an IndexError exception.
本文也稱之為這個對象的引用是unprotected。
Python源碼中,返回unprotected preferencess(borrowing a reference)的函數有:
PyTuple_GetItem(),
PyList_GetItem(),
PyList_GET_ITEM(),
PyList_SET_ITEM(),
PyDict_GetItem(),
PyDict_GetItemString(),
PyErr_Occurred(),
PyFile_Name(),
PyImport_GetModuleDict(),
PyModule_GetDict(),
PyImport_AddModule(),
PyObject_Init(),
Py_InitModule(),
Py_InitModule3(),
Py_InitModule4(), and
PySequence_Fast_GET_ITEM().
對於PyArg_ParseTuple()來說,這個函數有時候會返回PyObject,存在類型為PyObject*的參數中。比如sysmodule.c中的例子:
static PyObject * sys_getrefcount(PyObject *self, PyObject *args) { PyObject *arg; if (!PyArg_ParseTuple(args, "O:getrefcount", &arg)) return NULL; return PyInt_FromLong(arg->ob_refcnt); }
PyArg_ParseTuple源碼的實現中,沒有對arg的引用計數INCREF,所以arg是一個unprotected object,當sys_getrefcount返回時,arg不應當被DECREF。
這裡提供一個比較完整的功能函數,計算一個列表中的整數之和.
示例1:
long sum_list(PyObject *list) { int i, n; long total = 0; PyObject *item; n = PyList_Size(list); if (n < 0) return -1; /* Not a list */ /* Caller should use PyErr_Occurred() if a -1 is returned. */ for (i = 0; i < n; i++) { /* PyList_GetItem does not INCREF "item". "item" is unprotected. */ item = PyList_GetItem(list, i); /* Can't fail */ if (PyInt_Check(item)) total += PyInt_AsLong(item); } return total; }
PyList_GetItem()返回的item是PyObject類型,引用計數沒有被INCREF,所以函數done之後沒有對item進行DECREF。
示例2:
long sum_sequence(PyObject *sequence) { int i, n; long total = 0; PyObject *item; n = PySequence_Length(sequence); if (n < 0) return -1; /* Has no length. */ /* Caller should use PyErr_Occurred() if a -1 is returned. */ for (i = 0; i < n; i++) { /* PySequence_GetItem INCREFs item. */ item = PySequence_GetItem(sequence, i); if (item == NULL) return -1; /* Not a sequence, or other failure */ if (PyInt_Check(item)) total += PyInt_AsLong(item); Py_DECREF(item); } return total; }
與示例1不同,PySequnce_GetItem()的源碼實現中,對返回的item的引用計數進行了INCREF,所以在函數done時需要調用Py_DECREF(item)。
什麼時候不需要調用INCREF
1.對於函數中的局部變數,這些局部變數如果是PyObject對象的指針,沒有必要增加這些局部對象的引用計數。理論上,當有一個變數指向對象的時候,對象的引用計數會被+1,同時在變數離開作用域時,對象的引用計數會被-1,而這兩個操作是相互抵消的,最終對象的引用數沒有改變。使用引用計數真正的原因是防止對象在有變數指向它的時候被提前銷毀。
什麼時候需要調用INCREF
如果有任何的可能在某個對象上調用DECREF,那麼就需要保證該對象不能處於unprotected狀態。
1) 如果一個引用處於unprotected,可能會引起微妙的bug。一個常見的情況是,從list中取出元素對象,繼續操作它,但是不增加它的引用計數。PyList_GetItem 會返回一個 borrowed reference ,所以 item 處於未保護狀態。一些其他的操作可能會從 list 中將這個對象刪除(遞減它的引用計數,或者釋放它)。導致 item 成為一個懸垂指針。
bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); /* BUG! */ }
這個函數的功能:從list中取出第0個元素item(此時沒有遞增它的引用計數),然後替換list[1]為整數0,最後列印item.看起來很正常,沒有什麼問題,其實不然。
我們跟著PyList_SetItem函數的流程走一遍。list中所有元素的引用計數都是protected的,所以當把list[1]的元素替換時,必須將原來的元素的引用計數減少。假設原來的元素list[1]是一個用戶自定義的一個類,並且實現了__del__方法。如果這個類的instance的引用計數為1,當減少它的引用計數時,此instance會被釋放,會調用__del__方法。而__del__方法是python用戶自己寫的代碼,所以__del__可以是任意的python代碼,那麼是不是有可能做了某些操作導致list[0]的引用計數無效,比如在__del__方法中del list[0],假如list[0]的引用計數也是1,那麼list[0]會被釋放,而被釋放的item再次被作為參數傳遞給了PyObject_print()函數,此時會出現意想不到的行為。
解決的辦法也很多簡單:
no_bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); Py_INCREF(item); /* Protect item. */ PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); Py_DECREF(item); }
This is a true story. An older version of Python contained variants of this bug and someone spent a considerable amount of time in a C debugger to figure out why his __del__()
methods would fail...
2) 傳遞PyObject對象給函數,一般都是假設傳遞過來的對象的引用計數已經是protected,因此在函數內部不需要調用Py_INCREF。不過,如果想要參數存活到函數退出,可以調用Py_INCREF。
When you pass an object reference into another function, in general, the function borrows the reference from you -- if it needs to store it, it will use Py_INCREF() to become an independent owner.
PyDict_SetItem()就是這樣的例子,將某些東西存放在字典中,會將key和value的引用計數都加1.
而PyTuple_SetItem()和PyList_SetItem()與PyDict_SetItem()不同,他們接管傳遞給他們的對象(偷取一個引用)。
PyTuple_SetItem的原型是PyTuple_SetItem(atuple, i, item): 如果atuple[i]當前包含了一個PyObject,則將此PyObject DECREF,然後atuple[i]設置為item。 item並不會被INCREFed
如果PyTuple_SetItem插入元素失敗,會減少item的引用計數。同樣,PyTuple_GetItem不會增加返回的item的引用計數。
PyObject *t; PyObject *x; x = PyInt_FromLong(1L); PyTuple_SetItem(t, 0, x);
當x作為參數傳遞給PyTuple_SetItem函數時,那麼必須不能調用Py_DECREF,因為PyTuple_SetItem()函數實現中沒有增加x的引用計數,如果你此時人為減少x的引用計數,那麼tuple t中的元素item已經被釋放了。
當tuple t 被DECREFed,其裡面的元素都會被DECREFed。
PyTuple_SetItem這樣的設計主要是考慮到一個很常見的場景:創建一個新的對象來填充tuple或list。例如創建這樣一個tuple, (1, 2, "there")。 使用Python C API可以這樣做:
PyObject *t; t = PyTuple_New(3); PyTuple_SetItem(t, 0, PyInt_FromLong(1L)); PyTuple_SetItem(t, 1, PyInt_FromLong(2L)); PyTuple_SetItem(t, 2, PyString_FromString("three"));
Note: PyTuple_SetItem是設置tuple元素的唯一的方法。 PySequence_SetItem和PyObject_SetItem都會拒絕這樣做,因為tuple是一個不可變的數據類型。
創建list與創建tuple的介面類似,PyList_New()和PyList_SetItem(),有個區別是填充list的元素可以使用PySequence_SetItem(),但是PySequence_SetItem會增加傳入的item的引用計數。
PyObject *l, *x; l = PyList_New(3); x = PyInt_FromLong(1L); PySequence_SetItem(l, 0, x); Py_DECREF(x); x = PyInt_FromLong(2L); PySequence_SetItem(l, 1, x); Py_DECREF(x); x = PyString_FromString("three"); PySequence_SetItem(l, 2, x); Py_DECREF(x);
Python信奉極簡主義,上述創建tuple(list)和填充tuple(list)的代碼可以簡化為:
PyObject *t, *l; t = Py_BuildValue("(iis)", 1, 2, "three"); l = Py_BuildValue("[iis]", 1, 2, "three");
Two Examples:
Example 1:
PyObject* MyFunction(void) { PyObject* temporary_list=NULL; PyObject* return_this=NULL; temporary_list = PyList_New(1); /* Note 1 */ if (temporary_list == NULL) return NULL; return_this = PyList_New(1); /* Note 1 */ if (return_this == NULL) Py_DECREF(temporary_list); /* Note 2 */ return NULL; } Py_DECREF(temporary_list); /* Note 2 */ return return_this; }
Note1: PyList_New返回的object的引用計數為1
Note2: 因為temporary_list 在函數退出時不應該存在,所以在函數返回前必須DECREFed。
Example 2:
PyObject* MyFunction(void) { PyObject* temporary=NULL; PyObject* return_this=NULL; PyObject* tup; PyObject* num; int err; tup = PyTuple_New(2); if (tup == NULL) return NULL; err = PyTuple_SetItem(tup, 0, PyInt_FromLong(222L)); /* Note 1 */ if (err) { Py_DECREF(tup); return NULL; } err = PyTuple_SetItem(tup, 1, PyInt_FromLong(333L)); /* Note 1 */ if (err) { Py_DECREF(tup); return NULL; } temporary = PyTuple_Getitem(tup, 0); /* Note 2 */ if (temporary == NULL) { Py_DECREF(tup); return NULL; } return_this = PyTuple_Getitem(tup, 1); /* Note 3 */ if (return_this == NULL) { Py_DECREF(tup); /* Note 3 */ return NULL; } /* Note 3 */ Py_DECREF(tup); return return_this; }
Note1:如果PyTuple_SetItem失敗或者這個tuple引用計數變為0,那麼PyInt_FromLong創建的對象引用計數也被減少
Note2:PyTuple_GetItem不會增加返回的對象的引用計數
Note3:MyFunction沒有責任處理temporary的引用計數,不需要DECREF temporary