扩展Python模块系列(四)----引用计数问题的处理

时间:2022-02-11 12:19:43

承接上文,发现在使用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 != ) \
_Py_CHECK_REFCNT(op) \
else \
_Py_Dealloc((PyObject *)(op)); \
} while ()

另外一组考虑是对象为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 < || 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 = ;
PyObject *item; n = PyList_Size(list);
if (n < )
return -; /* Not a list */
/* Caller should use PyErr_Occurred() if a -1 is returned. */
for (i = ; 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 = ;
PyObject *item;
n = PySequence_Length(sequence);
if (n < )
return -; /* Has no length. */
/* Caller should use PyErr_Occurred() if a -1 is returned. */
for (i = ; i < n; i++) {
/* PySequence_GetItem INCREFs item. */
item = PySequence_GetItem(sequence, i);
if (item == NULL)
return -; /* 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, ); PyList_SetItem(list, , PyInt_FromLong(0L));
PyObject_Print(item, stdout, ); /* 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, );
Py_INCREF(item); /* Protect item. */ PyList_SetItem(list, , PyInt_FromLong(0L));
PyObject_Print(item, stdout, );
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.

扩展Python模块系列(四)----引用计数问题的处理

而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, , 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();
PyTuple_SetItem(t, , PyInt_FromLong(1L));
PyTuple_SetItem(t, , PyInt_FromLong(2L));
PyTuple_SetItem(t, , 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();
x = PyInt_FromLong(1L);
PySequence_SetItem(l, , x); Py_DECREF(x);
x = PyInt_FromLong(2L);
PySequence_SetItem(l, , x); Py_DECREF(x);
x = PyString_FromString("three");
PySequence_SetItem(l, , x); Py_DECREF(x);

Python信奉极简主义,上述创建tuple(list)和填充tuple(list)的代码可以简化为:

 PyObject *t, *l;

 t = Py_BuildValue("(iis)", , , "three");
l = Py_BuildValue("[iis]", , , "three");

Two Examples:

Example 1:

    PyObject*
MyFunction(void)
{
PyObject* temporary_list=NULL;
PyObject* return_this=NULL; temporary_list = PyList_New(); /* Note 1 */
if (temporary_list == NULL)
return NULL; return_this = PyList_New(); /* 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();
if (tup == NULL)
return NULL; err = PyTuple_SetItem(tup, , PyInt_FromLong(222L)); /* Note 1 */
if (err) {
Py_DECREF(tup);
return NULL;
}
err = PyTuple_SetItem(tup, , PyInt_FromLong(333L)); /* Note 1 */
if (err) {
Py_DECREF(tup);
return NULL;
} temporary = PyTuple_Getitem(tup, ); /* Note 2 */
if (temporary == NULL) {
Py_DECREF(tup);
return NULL;
} return_this = PyTuple_Getitem(tup, ); /* 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