[关闭]
@dream-cheny 2016-04-15T17:00:19.000000Z 字数 7539 阅读 8851

python 中的引用计数(reference counts)

python


为什么有引用计数?

一句话:为了在适当的时候回收 object 所占用的内存空间
什么动态申请内存呐,什么内存泄漏呐,相关软文太多太多了,这里只讨论 python 如何使用和管理 object 的引用计数。

管理计数的宏 Py_INCREF 和 Py_DECREF

有两个宏 Py_INCREF(op)Py_DECREF(op) 被用于 递增 和 递减 引用计数 reference counts。当引用计数到达0时,Py_DECREF() 会释放对象,但它不会直接调用 free(),相反,它会通过一个在 PyObject 中的类型对象 ob_type 的函数指针来调用 free()。出于这种目的,每一个对象都包含一个指向它的类型对象的指针。

看代码会更清晰:

  1. typedef void (*destructor)(PyObject *)
  2. typedef struct _typeobject {
  3. ...
  4. destructor tp_dealloc;
  5. ...
  6. } PyTypeObject;
  1. /* PyObject_HEAD defines the initial segment of every PyObject. */
  2. #define PyObject_HEAD \
  3. Py_ssize_t ob_refcnt; \
  4. struct _typeobject *ob_type;
  5. typedef struct _object {
  6. PyObject_HEAD
  7. } PyObject;

Py_DECREF() 通过函数指针 tp_dealloc 来释放对象。

在 python 中,你不能拥有一个对象,但你可以拥有对象的引用。对象的引用数 reference count 定义为所有引用它的数量。一个引用的所有者有责任在引用不再需要的时候调用 Py_DECREF()
引用的所有权可以被传递。

你有三种方式处理掉一个引用:
1. 将引用传递给别人
2. 将引用存储起来
3. 调用 Py_DECREF()

一个引用的所有者千万不要忘记处理它的引用。
为了防止内存泄露,每一次调用 Py_INCREF(),有责任对应的调用一次 Py_DECREF()。这种责任可以在函数之前传递(就是将引用传递给别人)。

什么时候使用 Py_INCREF 和 Py_DECREF

绝大多数 Python 对象是通过调用 python/c api 创建的。

这些 api 函数的原型:

  1. PyObject* Py_Something(arguments)

这些函数通常(但不必须)在返回一个新对象之前调用 Py_INCREF()。一般调用 Py_Something 的函数有责任去调用 Py_DECREF()。反过来,函数返回对象给调用者,同时也传递了引用的所有权给调用者。

Py_Something 创建一个引用,并将管理引用的责任传递给它的调用者,这被称为 new reference (我翻译成:实引用,即这个引用是被保护的)。

在 python 文档中,一般是这样体现的:

  1. PyObject* PyList_New(int len)
  2. Return value: New reference.
  3. Returns a new list of length len on success, or NULL on failure.

New reference 传达了两个密切相关的信息:
1. PyList_New 会返回一个指向 PyObject 的指针(引用)
2. 我们有责任 DECREF 它

来,上代码:

  1. void MyCode(arguments) {
  2. PyObject *pyo;
  3. ...
  4. pyo = Py_Something(args);
  5. ...
  6. Py_DECREF(pyo);
  7. }

MyCode 需要负责管理 Py_Something 传递回来的对象的引用。

另一方面,如果 MyCode 需要返回 pyo,则不能调用 Py_DECREF

  1. void MyCode(arguments) {
  2. PyObject *pyo;
  3. ...
  4. pyo = Py_Something(args);
  5. ...
  6. return pyo;
  7. }

这就是将引用传递给别人,丢出去之后,可以不管引用的死活了。

如果一个函数返回 None,则 C 代码应该是这样的:

  1. Py_INCREF(Py_None);
  2. return Py_None;

但有时 python 代码不需要调用 Py_DECREF()

  1. PyObject * PyTuple_GetItem(register PyObject *op, register int i)
  2. {
  3. if (!PyTuple_Check(op)) {
  4. PyErr_BadInternalCall();
  5. return NULL;
  6. }
  7. if (i < 0 || i >= ((PyTupleObject *)op) -> ob_size) {
  8. PyErr_SetString(PyExc_IndexError,
  9. "tuple index out of range");
  10. return NULL;
  11. }
  12. return ((PyTupleObject *)op) -> ob_item[i];
  13. }

这样情况被称为 borrowed reference,即这个引用是无保护的,此时不应该调用 Py_DECREF()

什么是 borrowed reference

指向一个对象的引用也可能是一个 borrowed reference (我翻译成:弱引用)。

这个很难用语言来表达,我的理解是:就像一个人在网盘上共享了一部电影,你可以通过一个链接(引用)操作这部电影,当你在线看得正起劲的时候,duang,电影的主人删除它,你这边心理上就当机了。

实际上, borrowed reference 真实存在这个问题("当机"),使用一个弱引用的时候,这个弱引用所引用的对象被它的所有者处理掉了,这里就可能会发生内存 crash 。

borrowed reference 的所有者不应该调用 Py_DECREF()

当然你可以把电影下载下来,变成你自己的(这就是下面要交待的)。

borrowed reference 可以变成一个 owned reference(我翻译成:实引用),通过调用 Py_INCREF()。这样并不会改变原始 borrowed reference 所有者的状态(而是创建一个新的 owned reference)。

当然 borrowed reference 也是有好处的,它不需要关心在代码流中处理引用,换句话说,使用 borrowed reference 在函数退出时不会出现内存泄露。

在 python 文档的,弱引用的表现形式:

  1. PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos)
  2. Return value: Borrowed reference.

示例:

  1. long sum_sequence(PyObject *sequence)
  2. {
  3. int i, n;
  4. long total = 0;
  5. PyObject *item;
  6. n = PySequence_Length(sequence);
  7. if (n < 0)
  8. return -1; /* Has no length. */
  9. /* Caller should use PyErr_Occurred() if a -1 is returned. */
  10. for (i = 0; i < n; i++) {
  11. /* PySequence_GetItem INCREFs item. */
  12. item = PySequence_GetItem(sequence, i);
  13. if (item == NULL)
  14. return -1; /* Not a sequence, or other failure */
  15. if (PyInt_Check(item))
  16. total += PyInt_AsLong(item);
  17. Py_DECREF(item);
  18. }
  19. return total;
  20. }

这个函数的作用是:计算整型 list 的所有 items 的和。

什么时候不需要 INCREF

对于每一个指向一个对象的指针的局部变量,不需要递增对象的引用计数。理论上,当有一个变量指向对象的时候,对象的引用计数会被+1,同时在变量离开作用域时,对象的引用计数会被-1,而这两个操作是相互抵消的,最终对象的引用数没有改变。

使用引用计数的唯一真正的原因是:当我们的变量指向一个对象时,防止这个对象被销毁。如果我们知道至少有一个其它的引用指向对象,直到我们的变量被销毁,那么就不需要临时递增对象的引用。

这出现在一个重要的应用场景中,如果对象作为参数传递给出由 python 调用的外部模块的 c functions, 调用机制保证在函数调用期间,对每一个参数都保留一个引用。

  1. long sum_list(PyObject *list)
  2. {
  3. int i, n;
  4. long total = 0;
  5. PyObject *item;
  6. n = PyList_Size(list);
  7. if (n < 0)
  8. return -1; /* Not a list */
  9. /* Caller should use PyErr_Occurred() if a -1 is returned. */
  10. for (i = 0; i < n; i++) {
  11. /* PyList_GetItem does not INCREF "item".
  12. "item" is unprotected. */
  13. item = PyList_GetItem(list, i); /* Can't fail */
  14. if (PyInt_Check(item))
  15. total += PyInt_AsLong(item);
  16. }
  17. return total;
  18. }

什么时候需要 INCREF

不要让一个对象处理未保护的状态 borrowed reference,如果对象处理未保护状态,它随时可能会被销毁。

borrowed reference 可能会引起微妙的 bug
一个普通的例子:从一个 list 获取对象,继续操作它,但并不递增它的引用。PyList_GetItem 会返回一个 borrowed reference ,所以 item 处于未保护状态。一些其他的操作可能会从 list 中将这个对象删除(递减它的引用计数,或者释放它)。导致 item 成为一个悬垂指针。

  1. bug(PyObject *list) {
  2. PyObject *item = PyList_GetItem(list, 0);
  3. PyList_SetItem(list, 1, PyInt_FromLong(0L));
  4. PyObject_Print(item, stdout, 0); /* BUG! */
  5. }
  1. no_bug(PyObject *list) {
  2. PyObject *item = PyList_GetItem(list, 0);
  3. Py_INCREF(item); /* Protect item. */
  4. PyList_SetItem(list, 1, PyInt_FromLong(0L));
  5. PyObject_Print(item, stdout, 0);
  6. Py_DECREF(item);
  7. }

传递对象给函数

到目前为止,我知道函数可以返回一个对象的引用,那么,当传递一个对象给函数的时候,会发生什么事情呢?

  1. int Caller(void) {
  2. PyObject* pyo;
  3. Function(pyo);
  4. ...
  5. }

绝大多数函数 假设 传递进来的参数已经被保护,因此在函数内部不需要调用 Py_INCREF(),除非函数想要参数存活到函数调用退出。

python 文档这样说:

  1. When you pass an object reference into another
  2. function, in general, the function borrows the reference from you
  3. -- if it needs to store it, it will use Py_INCREF() to become an
  4. independent owner.

PyDict_SetItem() 正是这样行为的一个例子。放置某些东西到字典中存储起来,因为它会 INCREF key 和 value。

PyTuple_SetItem 不走寻常路

恰好有两个重要的函数不会理会上面讨论的正常规则:PyTuple_SetItem()PyList_SetItem()
它们接管传递给他们的对象(即使是执行失败),python 文档使用了一个短语来表达接管的含义:偷取一个引用

PyTuple_SetItem() 的原型:

  1. int PyTuple_SetItem(PyObject *p, Py_ssize_t pos, PyObject *o)

调用它:PyTuple_SetItem(atuple, i, item), 是怎么做的呢?
如果 atuple[i] 当前包含一个 PyObject,那么 PyObject 会被 DECREF,然后 atuple[i] 被设置成 item,但 item 不会被 INCREF。

如果 PyTuple_SetItem 调用失败,它会递减 item 的引用计数。同样,PyTuple_GetItem() 也不会递增返回的 item 的引用计数。

隐含地PyTuple_SetItem 会从你手中抓取 item 的引用,如果 item 没有被保护,那么 item 可能会被 DECREF,同时 python 就会 crash。这就是弱引用埋下的雷。

来看一段代码:

  1. PyObject *t;
  2. PyObject *x;
  3. x = PyInt_FromLong(1L);
  4. PyTuple_SetItem(t, 0, x);

执行完第 3 条语句的时候,x 的引用数为1,如果不需要它了,正常应该调用 Py_DECREF(x),但调用了 PyTuple_SetItem,你就不能 DECREF x 了,因为当 tuple t 被 DECREF时,x 也会被 DECREF。

那么为什么 PyTuple_SetItem 会被设计成接管 item 的引用呢?
因为一个非常普遍的习惯是用新创建的对象填充一个 tuple 或者是 list。例如,可以像这样创建一个 tuple (1, 2, "three")(这里不考虑错误处理):

  1. PyObject *t;
  2. t = PyTuple_New(3);
  3. PyTuple_SetItem(t, 0, PyInt_FromLong(1L));
  4. PyTuple_SetItem(t, 1, PyInt_FromLong(2L));
  5. PyTuple_SetItem(t, 2, PyString_FromString("three"));

这就是 python 的极简主义,一名话 I like that

另:PyTuple_SetItem 是创建 tuple 的唯一途径,因为 tuple 是不可变的数据类型。

可以用与上面同样风格的代码创建一个 list,当然要用 PyList_New()PyList_SetItem()。与 tuple 不同的是,list 可以用 PySequence_SetItem() 创建,不过规则会有所不同:

  1. PyObject *l, *x;
  2. l = PyList_New(3);
  3. x = PyInt_FromLong(1L);
  4. PySequence_SetItem(l, 0, x); Py_DECREF(x);
  5. x = PyInt_FromLong(2L);
  6. PySequence_SetItem(l, 1, x); Py_DECREF(x);
  7. x = PyString_FromString("three");
  8. PySequence_SetItem(l, 2, x); Py_DECREF(x);

PySequence_SetItem 不会 “偷取” x 的引用,但会递增。

怎么看起来代码越来越多了,说好的极简主义呢?别急,往下看:

  1. PyObject *t, *l;
  2. t = Py_BuildValue("(iis)", 1, 2, "three");
  3. l = Py_BuildValue("[iis]", 1, 2, "three");

示例

  1. PyObject* MyFunction(void)
  2. {
  3. PyObject* temporary_list=NULL;
  4. PyObject* return_this=NULL;
  5. temporary_list = PyList_New(1); /* Note 1 */
  6. if (temporary_list == NULL)
  7. return NULL;
  8. return_this = PyList_New(1); /* Note 1 */
  9. if (return_this == NULL) {
  10. Py_DECREF(temporary_list); /* Note 2 */
  11. return NULL;
  12. }
  13. Py_DECREF(temporary_list); /* Note 2 */
  14. return return_this;
  15. }

Note1:PyList_New 返回的对象的引用数为1。
Note2:在 MyFunction 退出之后,temporary_list 将要消失,所以它必须被 DECREF。

  1. PyObject* MyFunction(void)
  2. {
  3. PyObject* temporary=NULL;
  4. PyObject* return_this=NULL;
  5. PyObject* tup;
  6. PyObject* num;
  7. int err;
  8. tup = PyTuple_New(2);
  9. if (tup == NULL)
  10. return NULL;
  11. err = PyTuple_SetItem(tup, 0, PyInt_FromLong(222L)); /* Note 1 */
  12. if (err) {
  13. Py_DECREF(tup);
  14. return NULL;
  15. }
  16. err = PyTuple_SetItem(tup, 1, PyInt_FromLong(333L)); /* Note 1 */
  17. if (err) {
  18. Py_DECREF(tup);
  19. return NULL;
  20. }
  21. temporary = PyTuple_Getitem(tup, 0); /* Note 2 */
  22. if (temporary == NULL) {
  23. Py_DECREF(tup);
  24. return NULL;
  25. }
  26. return_this = PyTuple_Getitem(tup, 1); /* Note 3 */
  27. if (return_this == NULL) {
  28. Py_DECREF(tup);
  29. /* Note 3 */
  30. return NULL;
  31. }
  32. /* Note 3 */
  33. Py_DECREF(tup);
  34. return return_this;
  35. }

Note1:如果 PyTuple_SetItem 失败,或者它创建的 tuple 被 DECREF 到 0,PyInt_FromLong 返回的对象会被 DECREF。
Note2:PyTuple_Getitem 没有递增返回对象的引用数。
Note3:不需要 DECREF 临时对象。

如何获取一个对象的引用计数

  1. static PyObject *
  2. sys_getrefcount(PyObject *self, PyObject *args)
  3. {
  4. PyObject *arg;
  5. if (!PyArg_ParseTuple(args, "O:getrefcount", &arg))
  6. return NULL;
  7. return PyInt_FromLong(arg->ob_refcnt);
  8. }

arg 是一个未保护对象,它不应该被 DECREF,因为它从来没有被 INCREF。

参考自:http://edcjones.tripod.com/refcount.html

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注