无法使用 ctypes.pythonapi 调整元组的大小

Can't resize a tuple using ctypes.pythonapi

仅用于测试,我尝试使用 ctypes 调整元组的大小,结果很糟糕:

Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import py_object, c_long, pythonapi
>>> _PyTuple_Resize = pythonapi._PyTuple_Resize
>>> _PyTuple_Resize.argtypes = (py_object, c_long)
>>> a = ()
>>> b = c_long(1)
>>> _PyTuple_Resize(a, b)
Segmentation fault (core dumped)

出了什么问题?

您的代码存在一些问题。

我们先从_PyTuple_Resize的签名说起,就是

int _PyTuple_Resize(PyObject **p, Py_ssize_t newsize)

即第一个参数不是 py_object(应该是 PyObject *p),而是 py_object passed by reference,这意味着:

from ctypes import POINTER, py_object, c_ssize_t, byref, pythonapi
_PyTuple_Resize = pythonapi._PyTuple_Resize
_PyTuple_Resize.argtypes = (POINTER(py_object), c_ssize_t)

然而,不需要定义_PyTuple_Resize的参数(和任何other pythonapi-function一样),如果不是int,只需要定义restype (但在 _PyTuple_Resize 的情况下)。

然后,上面链接的文档指出:

Because tuples are supposed to be immutable, this should only be used if there is only one reference to the object. Do not use this if the tuple may already be known to some other part of the code.

好吧,代码的其他部分都知道空元组:

import sys
a=()
sys.getrefcount(a)
# 28236

正如@CristiFati 在评论中指出的那样,这是一个小优化,可以做到这一点,因为元组是不可变的:所有空元组共享同一个单例。所以在空元组上使用 _PyTuple_Resize 是很有问题的,即使这个极端情况被捕获在 code of _PyTuple_Resize:

if (oldsize == 0) {
    /* Empty tuples are often shared, so we should never
       resize them in-place even if we do own the only
       (current) reference */
    Py_DECREF(v);
    *pv = PyTuple_New(newsize);
    return *pv == NULL ? -1 : 0;
}

但是,我的观点是必须确保在调用 _PyTuple_Resize.

之前没有其他引用

现在,即使对程序其他部分未知的元组使用 _PyTuple_Resize

b = c_ssize_t(2)
A=py_object(("no one knows me",))
pythonapi._PyTuple_Resize(byref(A), b) # returns 0 - means everything ok

我们得到一个处于不一致状态的对象:

print(A)
# py_object(('no one knows me', <NULL>))

问题是作为第二个元素的 NULL 指针:现在许多使用 A.value 的操作(如 print(A.value))将出现段错误或导致其他问题。

所以现在,需要使用 PyTuple_SetItem (it handles the NULL elements correctly and doesn't try to decrease reference of a NULL-pointer) to set NULL-elements in the tuple before anything can be done with A.value. Btw. usually one would use PyTuple_SET_ITEM for newly created tuple/elements, but it is a define 而不是 pythonapi 的一部分。

由于PyTuple_SetItem窃取了引用,所以我们也需要处理它:

B=py_object(666)
pythonapi.Py_IncRef(B)
pythonapi.PyTuple_SetItem(A,1,B)
print(A.value)
# ('no one knows me', 666)

对于小元组 _PyTuple_Resize 将始终(对于 64 位构建)创建一个新的元组对象而不重用旧的,因为添加一个元素意味着向内存占用空间添加 8 个字节(至少对于64 位构建)和 pymalloc returns 8 字节对齐的指针,因此与 不同,需要一个新对象:

b = c_ssize_t(2)
A=py_object(("no one knows me",))
print(id(A.value))
# 2311126190344
pythonapi._PyTuple_Resize(byref(A), b)
print(id(A.value))
# 2311143455304

我们看到了不同的ID!

但是,对于内存占用大于 512 字节的元组对象,内存由底层 c 运行时内存分配器管理,因此可以调整指针的大小:

b = c_ssize_t(1002)
A=py_object(("no one knows me",)*1000)
print(id(A.value))
# 2350988176984
pythonapi._PyTuple_Resize(byref(A), b)
print(id(A.value))
# 2350988176984

现在,旧对象已扩展 - 并保留了 id!