仅出于测试目的,我尝试使用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)
出了什么问题?
答案 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!