PyTuple_SetItem的限制

12 投票
2 回答
3437 浏览
提问于 2025-04-16 18:15

我有一个Python扩展模块,它创建了一个元组,并把这个元组作为另一个对象的属性,同时还设置了元组中的一些项。每次我在Python中执行这个模块时,都会遇到一个错误:SystemError: bad argument to internal function

我看了看PyTuple的文档,还调试了我的程序几个小时,还是搞不清楚到底出了什么问题。通过调试器运行我的程序时,发现问题出现在Python解释器内部的一个库调用中。最后,我查看了Python的源代码,终于意识到问题所在。PyTuple_SetItem这个函数有一个我之前不知道的有趣限制,而且在文档中找不到明确的说明。

这是Python源代码中重要的函数(为了清晰起见进行了编辑):

int PyTuple_SetItem(register PyObject *op, register Py_ssize_t i, PyObject *newitem)
{
    .....
    if (!PyTuple_Check(op) || op->ob_refcnt != 1) {
        Py_XDECREF(newitem);
        PyErr_BadInternalCall();
        return -1;
    }
    .....
}

这里重要的一行是条件op->ob_refcnt != 1。所以问题来了:你不能调用PyTuple_SetItem,除非这个元组的引用计数是1。看起来这个限制的意思是,你只能在使用PyTuple_New()创建元组之后,立刻调用PyTuple_SetItem。我想这也有道理,因为元组是不可变的,这个限制有助于让你的C代码更符合Python类型系统的抽象。

不过,我在任何地方都找不到这个限制的文档。相关的文档似乎在这里这里,但都没有说明这个限制。文档基本上说,当你调用PyTuple_New(X)时,元组中的所有项都会被初始化为NULL。由于NULL不是有效的Python值,所以扩展模块的程序员必须确保在将元组返回给解释器之前,所有的槽位都填入正确的Python值。但文档中并没有说明这必须在元组对象的引用计数为1时完成。

所以现在的问题是,我的代码基本上陷入了困境,因为我不知道这个(未记录的?)PyTuple_SetItem的限制。我的代码结构使得在元组成为另一个对象的属性之前,插入项非常不方便。因此,当我想填充元组中的项时,元组的引用计数已经更高了。

我可能需要重构我的代码,但我曾认真考虑过暂时将元组的引用计数设置为1,插入项,然后再恢复原来的引用计数。当然,我知道这是一种糟糕的黑客方式,并不是任何永久的解决方案。不管怎样,我想知道关于元组引用计数的要求是否在某个地方有文档说明。这是CPython的实现细节,还是API用户可以依赖的预期行为?

2 个回答

2

Python的C接口文档非常少,很多地方可能根本没有提到这个限制。

当然,一旦有东西拿到了元组,你就不应该去修改它。要么把你需要放进元组的元素直接传进去,要么就用列表来代替元组。

8

我很确定你可以通过使用 PyTuple_SET_ITEM 来绕过一些限制,而不是用 PyTuple_SetItemPyTuple_SET_ITEM 是在 tupleobject.h 文件中定义的一个宏,具体如下:

#define PyTuple_SET_ITEM(op, i, v) (((PyTupleObject*)(op))->ob_item[i] = v

所以,如果你绝对、肯定、完全确定以下几点:

  1. op 是一个元组对象
  2. 到目前为止,你还没有在元组中初始化槽位 i
  3. 你拥有一个对 v 的引用,并且你想让元组“偷走”它
  4. 在你调用 PyTuple_SET_ITEM 之前,没有其他 Python 对象会使用这个元组

那么我想你可以安全地使用 PyTuple_SET_ITEM

撰写回答