首页 > 解决方案 > 为什么 PySequence_GetItem 返回一个新的引用?

问题描述

我被 C/C++ python 扩展中的内存泄漏所困扰,因为假设它PySequence_GetItem会返回一个借用的引用,就像PyList_GetItemand一样PyTuple_GetItem。我的问题是:为什么PySequence_GetItem返回一个新的引用而PyList_GetItem返回PyTuple_GetItem借来的引用?

文档

PyObject* PySequence_GetItem(PyObject *o, Py_ssize_t i)
    Return value: New reference.
    Return the ith element of o, or NULL on failure. This is the equivalent of the Python expression o[i].

标签: cpython

解决方案


您从 获得新的引用PySequence,因为这是PySequence-protocol 定义的。

然而,以这种方式定义协议有充分的理由:并非所有序列都由内存支持(如list, tuple),因为某些项目是动态创建的(如range, unicode)。

因为listtuple所有项目都归list/拥有tuple(它们不是临时对象)所以我们可以借用它们(借用是一个小的优化) - list/tuple最终会释放内存。

range是序列的另一个例子。它实现了 PySequence-protocol):

static PySequenceMethods range_as_sequence = {
    (lenfunc)range_length,      /* sq_length */
    0,                          /* sq_concat */
    0,                          /* sq_repeat */
    (ssizeargfunc)range_item,   /* sq_item */
    0,                          /* sq_slice */
    0,                          /* sq_ass_item */
    0,                          /* sq_ass_slice */
    (objobjproc)range_contains, /* sq_contains */
};

但是,返回的对象PySequence_GetItem是临时的(即没有人在函数之外拥有对它的引用),我们可以在源代码中验证这一点range_item

static PyObject *
range_item(rangeobject *r, Py_ssize_t i)
{
    PyObject *res, *arg = PyLong_FromSsize_t(i);
    if (!arg) {
        return NULL;
    }
    res = compute_range_item(r, arg);
    Py_DECREF(arg);
    return res;
}

归结compute_range_itemcompute_item

static PyObject *
compute_item(rangeobject *r, PyObject *i)
{
    PyObject *incr, *result;
    /* PyLong equivalent to:
     *    return r->start + (i * r->step)
     */
    incr = PyNumber_Multiply(i, r->step);
    if (!incr)
        return NULL;
    result = PyNumber_Add(r->start, incr);
    Py_DECREF(incr);
    return result;
}

没有人拥有返回的对象result,因此接收者必须注意减少引用计数。

可能还有其他解决方案(某种对已创建项目的缓存),但返回新引用是处理动态创建项目问题的最简单/透明的方法。


推荐阅读