最后,我们来到 Python 的内存管理的重要主题。Python 有自己的垃圾收集器,但它只
是为了解决引用计数(reference counting)算法中的循环引用的问题。引用计数是管理垃圾
对象的重新分配的主要方法。
Python/C API 文档介绍了引用所有权,以解释它如何处理对象的释放。Python 中的对
象不是独占的,它们始终共享。对象的实际创建由 Python 的内存管理器管理。它是 CPython
解释器的组件,负责为存储在私有堆中的对象分配和释放内存。它可以持有对象的引用。
由引用(PyObject*指针)表示的 Python 中的每个对象都有一个相关的引用计数。
当它变为 0 时,它意味着没有对象持有该对象的任何有效引用,就可以调用与其类型相关
联的释放器。Python/C API 提供了两个宏来增加和减少引用计数:Py_INCREF()和
Py_DECREF()。但在我们讨论它们的细节之前,我们需要了解更多关于引用所有权的术语。
● 传递所有权(Passing of ownership):每当我们说函数将所有权传递给引用时,这
意味着它已经增加了引用计数,并且当不再需要该引用对象时,调用者负责减少计
数。大多数函数返回新创建的对象,例如 Py_BuildValue,就是这样做的。如果
我们把函数返回的对象传递给另一个调用者,则所有权再次传递。在这种情况下,
我们不减少引用计数,因为它不再是我们的责任。这就是为什么 fibonacci_py()
函数不会对 result 变量调用 Py_DECREF()。
● 借用引用(Borrowed references):引用的借用发生在函数接收到对某个 Python 对
象的引用作为参数时。不应该在该函数中减少此引用的引用计数,除非它的范围明
确增加。在我们的 fibonacci_py()函数中,self 和 args 参数是这样借用引
用,因此我们不对它们调用 PyDECREF()。一些 Python/C API 函数也可能返回借用引用。值得注意的例子是 PyTuple_GetItem()和 PyList_GetItem()。人
们经常说,这种引用是无保护的。没有必要处理它的所有权,除非它将作为函数的
返回值返回。在大多数情况下,如果我们使用这样的借用引用作为其他 Python/C
API 调用的参数,应该格外小心。在某些情况下,用作其他函数的参数之前,可能
需 要 使 用 Py_INCREF() 额 外 地 保 护 这 些 引 用 , 并 且 在 不 再 需 要 时 调 用
Py_DECREF()。
● 盗用引用(Stolen references):Python/C API 函数也有可能盗用引用,而不是在作
为调用参数时借用它。有两个函数的就是这种情况:pytuple_setitem()和
pylist_setitem()。它们完全接管传递给他们的引用。它们本身不增加引用计
数,但是当不再需要引用时,将调用 py_decref()。
关注引用计数是编写复杂扩展时最困难的事情之一。一些不那么明显的问题可能不会
被注意到,直到代码在多线程环境中运行。
另一个常见的问题是由 Python 对象模型的本质引起的,事实上,一些函数返回的是借
用引用。当引用计数变为 0 时,释放函数会执行。对于用户定义的类,可以定义一个
del()方法,该方法会在此时调用。这可以是任何 Python 代码,它可能会影响其他
对象及其引用计数。Python 的官方文档给出了可能受此问题影响的代码,示例如下:
void bug(PyObject *list) {
PyObject item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); / BUG! */
}
它看起来似乎完全没问题,但实际上我们不知道 list 对象包含什么元素。当 PyList_
SetItem()在 list[1]索引上设置新值时,之前存储在该索引处的对象的所有权会被处
理。如果它是唯一存在的引用,引用计数将变为 0,并且对象将被释放。这可能是一些用
户定义的类,该类中实现了自定义的__del__()方法。如果这样的__del__()执行,
item[0]就会从列表中移除,此时就会发生严重的问题。注意 PyList_GetItem()返回
一个借用引用!它在返回引用之前不需要调用 Py_INCREF()。因此在该代码中,可能会
调用 PyObject_Print(),并引用不再存在的对象。这将导致段错误,同时也会导致
Python 解释器崩溃。
正确的方法是在我们需要它们的整个时间段里保护借用引用,因为任何调用都可能导
致任何其他对象的释放,即使它们看起来不相关,如下所示:
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 高手编程系列七百六十二:引用计数
最新推荐文章于 2024-11-11 21:30:41 发布