Невозможно изменить размер кортежа с помощью 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)

Что пошло не так?


person Marco Sulla    schedule 08.12.2019    source источник


Ответы (1)


Есть некоторые проблемы с вашим кодом.

Начнем с подписи _PyTuple_Resize, это

int _PyTuple_Resize(PyObject **p, Py_ssize_t newsize)

т. е. первым аргументом является не py_object (который был бы PyObject *p), а py_object передаются по ссылке, что означает:

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 (как любой другая функция pythonapi), нужно определить restype только в том случае, если это не int (но это в случае _PyTuple_Resize).

Затем в приведенной выше связанной документации говорится:

Поскольку кортежи предполагаются неизменяемыми, это следует использовать только в том случае, если на объект имеется только одна ссылка. Не используйте это, если кортеж уже может быть известен какой-либо другой части кода.

Что ж, пустой кортеж хорошо известен другим частям кода:

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

Как отметил @CristiFati в комментариях, это небольшая оптимизация, которую можно выполнить, потому что кортежи неизменяемы: все пустые кортежи используют один и тот же синглтон. Таким образом, использование _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 будут segfault или приведут к другим проблемам.

Итак, теперь нужно использовать PyTuple_SetItem (это правильно обрабатывает элементы NULL и не пытается уменьшить ссылку на NULL-указатель) для установки NULL элементов в кортеже, прежде чем что-либо можно будет сделать с A.value. Кстати. обычно можно использовать PyTuple_SET_ITEM для вновь созданных кортежей/элементов, но это определение и, следовательно, не является частью 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 возвращает 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

Мы видим разные идентификаторы!

Однако для объектов-кортежей с объемом памяти более 512 байт память управляется базовым распределителем памяти c-runtime, и поэтому возможно изменение размера указателя:

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

Теперь старый объект расширяется, а идентификатор сохраняется!

person ead    schedule 08.12.2019
comment
sys.getrefcount вернул это значение, поскольку () является одноэлементным (как и многие другие: None, True, < i>-1, 0, 1, 2,, ...) - person CristiFati; 09.12.2019
comment
Существует альтернатива созданию произвольного непустого кортежа: pastebin.com/esDZbCJh Затем вы можете изменить его размер, и он будет содержать только NULLs, которые нужно заполнить PyTuple_SetItem (не PyTuple_SET_ITEM) - person Marco Sulla; 09.12.2019
comment
@MarcoSulla Вы правы, я ошибочно предположил, что PyTuple_SetItem не может обрабатывать NULL (обычно PyTuple_SET_ITEM используется для назначения вновь созданных элементов). Однако использование PyTuple_New не то же самое, что _PyTuple_Resize, но вы хотели создать новый кортеж, а не изменять размер существующего? - person ead; 09.12.2019
comment
Да, я хочу создать его пустым, изменить его размер и заполнить. Вкратце, я хочу посмотреть, будет ли делать что-то подобное быстрее, чем заполнять список и преобразовывать его в кортеж. Вероятно, мне нужно настроить размер изменения размера... Мне нужно выполнить несколько тестов и проверить код CPython и посмотреть, как используется _PyTuple_Resize. - person Marco Sulla; 09.12.2019
comment
Список @MarcoSulla перегружен, поэтому средняя стоимость добавления элемента составляет O (1). Проблема кортежей: нет разницы между размером кортежа и вместимостью кортежа (поэтому мы можем увеличить размер без необходимости перераспределения). Таким образом, вы должны перераспределять память для каждого добавления, и поэтому вы должны зависеть от способности диспетчера памяти c-runtime перераспределять память без слишком частого перемещения данных, но из-за фрагментации памяти время выполнения добавления элемента может стать в среднем O(n). Это сложно, потому что проблема может возникнуть только после нескольких часов работы. - person ead; 09.12.2019
comment
Могу ли я выделить также кортеж? - person Marco Sulla; 09.12.2019
comment
@MarcoSulla По крайней мере, не из коробки: list-object содержит информацию о емкости базового C-массива github.com/python/cpython/blob/master/Include/listobject.h#L39, кортеж не имеет: github.com/python/cpython/blob/master/Include/cpython/ - person ead; 09.12.2019
comment
Я заменил PySequence_Tuple на abstract.c, что эквивалентно tuple(iterable). Это кажется очень простым. Он начинается с пустого кортежа размером n=10, затем каждый раз, когда вам нужно больше места, n увеличивается на 10 и на 25%. Это более агрессивная стратегия, чем списки. Я попробую и посмотрю скамейки. - person Marco Sulla; 09.12.2019