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

时间:2019-12-08 19:57:36

标签: python tuples ctypes python-c-api

仅出于测试目的,我尝试使用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)

出了什么问题?

1 个答案:

答案 0 :(得分:3)

您的代码存在一些问题。

让我们以_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一样),如果不是restype,则只需定义int(但是_PyTuple_Resize)。

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

  

由于元组被假定为不可变的,因此仅在对该对象只有一个引用的情况下才应使用它。如果该代码的其他部分可能已经知道该元组,请不要使用它。

好,代码的其他部分也很清楚空元组:

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

正如@CristiFati在评论中指出的那样,这是一个小的优化,之所以可以这样做是因为元组是不可变的:所有空元组共享同一单例。因此,即使在code of _PyTuple_Resize中遇到了这种极端情况,在空元组上使用_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指针是第二个元素:现在,许多print(A.value)的操作(如A.value)将发生段错误或导致其他问题。

因此,现在,需要使用PyTuple_SetItem(它正确处理NULL元素,并且不尝试减少对NULL指针的引用)来设置NULL元素A.value可以完成任何操作之前的元组。顺便说一句。通常,新创建的元组/元素将使用PyTuple_SET_ITEM,但是它是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个字节(至少对于64bit-构建),并且pymalloc返回8字节对齐的指针,因此与adding chars to string不同,将需要一个新对象:

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!