LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 243|回复: 0

对象、类型和引用计数

[复制链接]
发表于 2024-1-21 23:52:56 | 显示全部楼层 |阅读模式

多数 Python/C API 函数都有一个或多个参数以及一个 PyObject* 类型的返回值。 这种类型是指向任意 Python 对象的不透明数据类型的指针。 由于所有 Python 对象类型在大多数情况下都被 Python 语言用相同的方式处理(例如,赋值、作用域规则和参数传递等),因此用单个 C 类型来表示它们是很适宜的。 几乎所有 Python 对象都存在于堆中:你不可声明一个类型为 PyObject 的自动或静态的变量,只能声明类型为 PyObject* 的指针变量。 唯一的例外是 type 对象;因为这种对象永远不能被释放,所以它们通常都是静态的 PyTypeObject 对象。

所有 Python 对象(甚至 Python 整数)都有一个 type 和一个 reference count。对象的类型确定它是什么类型的对象(例如整数、列表或用户定义函数;还有更多,如 标准类型层级结构 中所述)。对于每个众所周知的类型,都有一个宏来检查对象是否属于该类型;例如,当(且仅当) a 所指的对象是 Python 列表时 PyList_Check(a) 为真。

引用计数
引用计数之所以重要是因为现有计算机的内存大小是有限的(并且往往限制得很严格);它会计算有多少不同的地方对一个对象进行了 strong reference。 这些地方可以是另一个对象,也可以是全局(或静态)C 变量,或是某个 C 函数中的局部变量。 当某个对象的最后一个 strong reference 被释放时(即其引用计数变为零),该对象就会被取消分配。 如果该对象包含对其他对象的引用,则会释放这些引用。 如果不再有对其他对象的引用,这些对象也会同样地被取消分配,依此类推。 (在这里对象之间的相互引用显然是个问题;目前的解决办法,就是“不要这样做”。)

对于引用计数总是会显式地执行操作。 通常的做法是使用 Py_INCREF() 宏来获取对象的新引用(即让引用计数加一),并使用 Py_DECREF() 宏来释放引用(即让引用计数减一)。 Py_DECREF() 宏比 incref 宏复杂得多,因为它必须检查引用计数是否为零然后再调用对象的释放器。 释放器是一个函数指针,它包含在对象的类型结构体中。 如果对象是复合对象类型,如列表,则特定于类型的释放器会负责释放对象中包含的其他对象的引用,并执行所需的其他终结化操作。 引用计数不会发生溢出;用于保存引用计数的位数至少会与虚拟内存中不同内存位置的位数相同 (假设 sizeof(Py_ssize_t) >= sizeof(void*))。 因此,引用计数的递增是一个简单的操作。

没有必要为每个包含指向对象指针的局部变量持有 strong reference (即增加引用计数)。 理论上说,当变量指向对象时对象的引用计数就会加一,而当变量离开其作用域时引用计数就会减一。 不过,这两种情况会相互抵消,所以最后引用计数并没有改变。 使用引用计数的唯一真正原因在于只要我们的变量指向对象就可以防止对象被释放。 只要我们知道至少还有一个指向某对象的引用与我们的变量同时存在,就没有必要临时获取一个新的 strong reference (即增加引用计数)。 出现引用计数增加的一种重要情况是对象作为参数被传递给扩展模块中的 C 函数而这些函数又在 Python 中被调用;调用机制会保证在调用期间对每个参数持有一个引用。

然而,一个常见的陷阱是从列表中提取对象并在不获取新引用的情况下将其保留一段时间。 某个其他操作可能在无意中从列表中移除该对象,释放这个引用,并可能撤销分配其资源。 真正的危险在于看似无害的操作可能会发起调用任意的 Python 代码来做这件事;有一条代码路径允许控制权从 Py_DECREF() 流回到用户,因此几乎任何操作都有潜在的危险。

安全的做法是始终使用泛型操作(名称以 PyObject_, PyNumber_, PySequence_ 或 PyMapping_ 开头的函数)。 这些操作总是为其返回的对象创建一个新的 strong reference (即增加引用计数)。 这使得调用者有责任在获得结果之后调用 Py_DECREF();这种做法很快就能习惯成自然。

引用计数细节
Python/C API 中函数的引用计数最好是使用 引用所有权 来解释。 所有权是关联到引用,而不是对象(对象不能被拥有:它们总是会被共享)。 “拥有一个引用”意味着当不再需要该引用时必须在其上调用 Py_DECREF。 所有权也可以被转移,这意味着接受该引用所有权的代码在不再需要它时必须通过调用 Py_DECREF() 或 Py_XDECREF() 来最终释放它 --- 或是继续转移这个责任(通常是转给其调用方)。 当一个函数将引用所有权转给其调用方时,则称调用方收到一个 新的 引用。 当未转移所有权时,则称调用方是 借入 这个引用。 对于 borrowed reference 来说不需要任何额外操作。

相反地,当调用方函数传入一个对象的引用时,存在两种可能:该函数 窃取 了一个对象的引用,或是没有窃取。 窃取引用 意味着当你向一个函数传入引用时,该函数会假定它拥有该引用,而你将不再对它负有责任。

很少有函数会窃取引用;两个重要的例外是 PyList_SetItem() 和 PyTuple_SetItem(),它们会窃取对条目的引用(但不是条目所在的元组或列表!)。 这些函数被设计为会窃取引用是因为在使用新创建的对象来填充元组或列表时有一个通常的惯例;例如,创建元组 (1, 2, "three") 的代码看起来可以是这样的(暂时不要管错误处理;下面会显示更好的代码编写方式):

PyObject *t;

t = PyTuple_New(3);
PyTuple_SetItem(t, 0, PyLong_FromLong(1L));
PyTuple_SetItem(t, 1, PyLong_FromLong(2L));
PyTuple_SetItem(t, 2, PyUnicode_FromString("three"));
在这里,PyLong_FromLong() 返回了一个新的引用并且它立即被 PyTuple_SetItem() 所窃取。 当你想要继续使用一个对象而对它的引用将被窃取时,请在调用窃取引用的函数之前使用 Py_INCREF() 来抓取另一个引用。

顺便提一下,PyTuple_SetItem() 是设置元组条目的 唯一 方式;PySequence_SetItem() 和 PyObject_SetItem() 会拒绝这样做因为元组是不可变数据类型。 你应当只对你自己创建的元组使用 PyTuple_SetItem()。

等价于填充一个列表的代码可以使用 PyList_New() 和 PyList_SetItem() 来编写。

然而,在实践中,你很少会使用这些创建和填充元组或列表的方式。 有一个通用的函数 Py_BuildValue() 可以根据 C 值来创建大多数常用对象,由一个 格式字符串 来指明。 例如,上面的两个代码块可以用下面的代码来代替(还会负责错误检测):

PyObject *tuple, *list;

tuple = Py_BuildValue("(iis)", 1, 2, "three");
list = Py_BuildValue("[iis]", 1, 2, "three");
在对条目使用 PyObject_SetItem() 等操作时更常见的做法是只借入引用,比如将参数传递给你正在编写的函数。 在这种情况下,它们在引用方面的行为更为清晰,因为你不必为了把引用转走而获取一个新的引用(“让它被偷取”)。 例如,这个函数将列表(实际上是任何可变序列)中的所有条目都设为给定的条目:

int
set_all(PyObject *target, PyObject *item)
{
    Py_ssize_t i, n;

    n = PyObject_Length(target);
    if (n < 0)
        return -1;
    for (i = 0; i < n; i++) {
        PyObject *index = PyLong_FromSsize_t(i);
        if (!index)
            return -1;
        if (PyObject_SetItem(target, index, item) < 0) {
            Py_DECREF(index);
            return -1;
        }
        Py_DECREF(index);
    }
    return 0;
}
对于函数返回值的情况略有不同。 虽然向大多数函数传递一个引用不会改变你对该引用的所有权责任,但许多返回一个引用的函数会给你该引用的所有权。 原因很简单:在许多情况下,返回的对象是临时创建的,而你得到的引用是对该对象的唯一引用。 因此,返回对象引用的通用函数,如 PyObject_GetItem() 和 PySequence_GetItem(),将总是返回一个新的引用(调用方将成为该引用的所有者)。

一个需要了解的重点在于你是否拥有一个由函数返回的引用只取决于你所调用的函数 --- 附带物 (作为参数传给函数的对象的类型) 不会带来额外影响! 因此,如果你使用 PyList_GetItem() 从一个列表提取条目,你并不会拥有其引用 --- 但是如果你使用 PySequence_GetItem() (它恰好接受完全相同的参数) 从同一个列表获取同样的条目,你就会拥有一个对所返回对象的引用。

下面是说明你要如何编写一个函数来计算一个整数列表中条目的示例;一个是使用 PyList_GetItem(),而另一个是使用 PySequence_GetItem()。

long
sum_list(PyObject *list)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;

    n = PyList_Size(list);
    if (n < 0)
        return -1; /* Not a list */
    for (i = 0; i < n; i++) {
        item = PyList_GetItem(list, i); /* Can't fail */
        if (!PyLong_Check(item)) continue; /* Skip non-integers */
        value = PyLong_AsLong(item);
        if (value == -1 && PyErr_Occurred())
            /* Integer too big to fit in a C long, bail out */
            return -1;
        total += value;
    }
    return total;
}
long
sum_sequence(PyObject *sequence)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;
    n = PySequence_Length(sequence);
    if (n < 0)
        return -1; /* Has no length */
    for (i = 0; i < n; i++) {
        item = PySequence_GetItem(sequence, i);
        if (item == NULL)
            return -1; /* Not a sequence, or other failure */
        if (PyLong_Check(item)) {
            value = PyLong_AsLong(item);
            Py_DECREF(item);
            if (value == -1 && PyErr_Occurred())
                /* Integer too big to fit in a C long, bail out */
                return -1;
            total += value;
        }
        else {
            Py_DECREF(item); /* Discard reference ownership */
        }
    }
    return total;
}
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表