Разработка C-расширений для эффективного управления списками Python
Способы работы со списками Python в коде на C
Как организовать базовый доступ к элементам списка через индекс?
Основной и наиболее эффективный способ взаимодействия со списком Python в C предполагает использование функций PyList_GetItem и PyList_SetItem. Этот подход даёт прямой доступ к объектам внутри списка без лишних преобразований.
Пример функции, которая суммирует все целые числа из переданного списка:
static PyObject* sum_list(PyObject* self, PyObject* args) {
PyObject* list;
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &list))
return NULL;
Py_ssize_t size = PyList_Size(list);
long total = 0;
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(list, i);
if (!PyLong_Check(item)) {
PyErr_SetString(PyExc_TypeError, "All items must be int");
return NULL;
}
total += PyLong_AsLong(item);
}
return PyLong_FromLong(total);
}
Python list in c (использование списков python в c)
Пошаговые пояснения: аргумент ожидается как список (проверка через PyArg_ParseTuple с форматом "O!"). Далее размер списка получается вызовом PyList_Size. В цикле каждый элемент извлекается без увеличения счётчика ссылок (borrowed reference). Проверка типа и извлечение значения с помощью PyLong_AsLong.
Типичные ошибки: забыть проверить тип элемента – может вызвать сбой при преобразовании. Неверное использование PyList_GetItem после изменений списка (список может быть изменён другим потоком) – рекомендуется удерживать GIL. Утечка ссылок при работе с элементами, если используется PyList_GetItem в сочетании с Py_INCREF (здесь borrowed, поэтому не нужно).
Как обработать список, не требуя обязательного типа списка (принять кортеж или любую последовательность)?
Если функция должна принимать не только список, но и кортеж или другую последовательность, удобно использовать PySequence_Fast. Эта функция преобразует любой итерируемый объект во внутреннее представление, гарантирующее быстрый доступ по индексу.
static PyObject* sum_sequence(PyObject* self, PyObject* args) {
PyObject* seq;
if (!PyArg_ParseTuple(args, "O", &seq))
return NULL;
PyObject* fast = PySequence_Fast(seq, "argument must be iterable");
if (!fast) return NULL;
Py_ssize_t size = PySequence_Fast_GET_SIZE(fast);
long total = 0;
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PySequence_Fast_GET_ITEM(fast, i);
if (!PyLong_Check(item)) {
PyErr_SetString(PyExc_TypeError, "All items must be int");
Py_DECREF(fast);
return NULL;
}
total += PyLong_AsLong(item);
}
Py_DECREF(fast);
return PyLong_FromLong(total);
}
Use python in c (использование python в коде c (встраивание))
Пояснения: PySequence_Fast возвращает новый объект (или тот же, если он уже быстрый). Владельцем ссылки становится вызывающий, поэтому в конце необходим Py_DECREF. Доступ к элементам через макросы PySequence_Fast_GET_ITEM.
Проблемы: при передаче большого списка может быть создана копия (например, если передан итератор). Это неэффективно по памяти. В таких случаях лучше сразу требовать список и отказываться от других типов.
Как создать новый список в C и вернуть его в Python?
Построение списка в C часто требуется для возврата набора результатов. Используется PyList_New для создания пустого списка или списка заданной длины, а затем PyList_SetItem или PyList_Append для заполнения.
static PyObject* make_double_list(PyObject* self, PyObject* args) {
PyObject* list_in;
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &list_in))
return NULL;
Py_ssize_t n = PyList_Size(list_in);
PyObject* list_out = PyList_New(n);
if (!list_out) return NULL;
for (Py_ssize_t i = 0; i < n; i++) {
PyObject* item = PyList_GetItem(list_in, i);
PyObject* doubled = PyNumber_Multiply(item, PyLong_FromLong(2));
if (!doubled) {
Py_DECREF(list_out);
return NULL;
}
PyList_SetItem(list_out, i, doubled); // steals reference
}
return list_out;
}
Python c types (библиотека ctypes в python)
Важный момент: PyList_SetItem крадёт ссылку на элемент (steals reference), поэтому после вызова не требуется Py_DECREF для doubled. Однако если происходит ошибка внутри цикла, нужно освободить уже созданный список.
Ошибки: забыть уменьшить счётчик ссылок на list_out при ошибке – утечка. Неправильное использование PyList_Append (он увеличивает счётчик ссылок на добавляемый объект, поэтому его нужно уменьшать после).
Как изменить существующий список на месте?
Для модификации элементов списка без создания нового объекта используется PyList_SetItem. Функция заменяет элемент по индексу, крадя ссылку на новый объект и освобождая старый.
static PyObject* negate_list(PyObject* self, PyObject* args) {
PyObject* list;
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &list))
return NULL;
Py_ssize_t n = PyList_Size(list);
for (Py_ssize_t i = 0; i < n; i++) {
PyObject* item = PyList_GetItem(list, i);
PyObject* neg = PyNumber_Negative(item);
if (!neg) return NULL;
PyList_SetItem(list, i, neg); // steals ref, old item DECREFed
}
Py_RETURN_NONE;
}
Python load c lib (загрузка c библиотеки в python)
Особенности: после PyList_SetItem старый элемент автоматически освобождается, поэтому не нужно вызывать Py_DECREF отдельно. Такой подход позволяет эффективно работать со списками без копирования.
Риски: модификация списка во время итерации по нему (если в другом потоке) может привести к повреждению данных. Рекомендуется использовать блокировку GIL или создавать копию, если изменения небезопасны.
Как обработать ошибки при работе с элементами списка?
При извлечении элементов через PyList_GetItem ошибка индекса не генерируется (функция возвращает NULL только при неверном индексе). Но чаще ошибки возникают при преобразовании типов. Следует всегда проверять успешность операций и вызывать PyErr_SetString с описанием.
static PyObject* safe_sum(PyObject* self, PyObject* args) {
PyObject* list;
if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &list))
return NULL;
Py_ssize_t n = PyList_Size(list);
long total = 0;
for (Py_ssize_t i = 0; i < n; i++) {
PyObject* item = PyList_GetItem(list, i);
if (item == NULL) { // rarely happens if list unchanged
return NULL;
}
PyObject* num = PyNumber_Long(item);
if (!num) {
PyErr_SetString(PyExc_TypeError, "item cannot be converted to int");
return NULL;
}
total += PyLong_AsLong(num);
Py_DECREF(num);
}
return PyLong_FromLong(total);
}
Примечание: PyNumber_Long принимает любой объект, поддерживающий преобразование. Это более гибко, чем прямая проверка PyLong_Check.
Типичная ошибка: не освобождать временные объекты (например, num) – утечка памяти. Также неверно полагаться, что PyList_GetItem всегда возвращает корректный указатель – при параллельном изменении списка он может указывать на удалённый объект.
Расширенные примеры: работа с многомерными списками и буферами
Ниже приведены примеры, демонстрирующие нестандартные случаи использования списков Python в C.
Пример 1: суммирование элементов вложенного списка (рекурсивный обход)
Функция рекурсивно обходит список, который может содержать другие списки, и суммирует все числовые элементы. Используется проверка типа через PyList_Check.
static PyObject* deep_sum(PyObject* self, PyObject* args) {
PyObject* obj;
if (!PyArg_ParseTuple(args, "O", &obj))
return NULL;
return deep_sum_helper(obj);
}
static PyObject* deep_sum_helper(PyObject* item) {
if (PyLong_Check(item) || PyFloat_Check(item)) {
return PyNumber_Add(item, PyLong_FromLong(0)); // copy
}
if (!PyList_Check(item)) {
PyErr_SetString(PyExc_TypeError, "unsupported type");
return NULL;
}
Py_ssize_t n = PyList_Size(item);
PyObject* total = PyLong_FromLong(0);
for (Py_ssize_t i = 0; i < n; i++) {
PyObject* sub = PyList_GetItem(item, i);
PyObject* subsum = deep_sum_helper(sub);
if (!subsum) {
Py_DECREF(total);
return NULL;
}
PyObject* new_total = PyNumber_Add(total, subsum);
Py_DECREF(total);
Py_DECREF(subsum);
total = new_total;
}
return total;
}
>>> deep_sum([1, [2, [3, 4]], 5]) 15
Пояснения: вспомогательная функция deep_sum_helper вызывает саму себя для каждого элемента. В конце возвращается сумма всех чисел.
Проблема: глубокая рекурсия может привести к переполнению стека C. Для больших вложенностей лучше реализовать итеративный обход.
Пример 2: конвертация списка Python в массив C (через буферный протокол)
Если список содержит однородные числовые данные (например, все float), можно получить прямой доступ к памяти через PyObject_GetBuffer. Это требует поддержки буферного протокола от объектов списка (обычно используют array.array или numpy.ndarray). Для обычного списка буфер не предоставляется, поэтому пример использует array.array.
static PyObject* sum_array_buffer(PyObject* self, PyObject* args) {
PyObject* array_obj;
if (!PyArg_ParseTuple(args, "O", &array_obj))
return NULL;
Py_buffer view;
if (PyObject_GetBuffer(array_obj, &view, PyBUF_FORMAT | PyBUF_ND) != 0)
return NULL;
if (view.format[0] != 'd' && view.format[0] != 'f') {
PyErr_SetString(PyExc_TypeError, "only float or double arrays");
PyBuffer_Release(&view);
return NULL;
}
double sum = 0.0;
if (view.format[0] == 'd') {
double* data = (double*)view.buf;
for (Py_ssize_t i = 0; i < view.len / (Py_ssize_t)sizeof(double); i++) {
sum += data[i];
}
} else {
float* data = (float*)view.buf;
for (Py_ssize_t i = 0; i < view.len / (Py_ssize_t)sizeof(float); i++) {
sum += data[i];
}
}
PyBuffer_Release(&view);
return PyFloat_FromDouble(sum);
}
>>> from array import array
>>> a = array('d', [1.5, 2.5, 3.0])
>>> sum_array_buffer(a)
7.0
Пояснения: буферный протокол позволяет получить сырой указатель на данные. Важно проверять формат (view.format) и размер элемента.
Ошибка: попытка использовать буфер с обычным списком вызовет исключение. Необходимо убедиться, что переданный объект поддерживает буфер, либо использовать альтернативные способы (например, PyList_GetItem в цикле).
Пример 3: передача списка через ctypes (внешняя библиотека)
Хотя это не прямой Python/C API, ctypes часто используется для взаимодействия с C. Можно передавать указатель на массив C, полученный из списка Python.
// C-функция, ожидающая массив int и размер
int sum_array(int* arr, int len) {
int s = 0;
for (int i = 0; i < len; i++) s += arr[i];
return s;
}
Python side:
>>> import ctypes
>>> lib = ctypes.CDLL('./mylib.so')
>>> lib.sum_array.argtypes = (ctypes.POINTER(ctypes.c_int), ctypes.c_int)
>>> lib.sum_array.restype = ctypes.c_int
>>> lst = [10, 20, 30]
>>> arr = (ctypes.c_int * len(lst))(*lst)
>>> lib.sum_array(arr, len(lst))
60
Пояснения: ctypes создаёт массив C из списка. Но это копирование данных, а не прямой доступ к списку Python. Такой подход удобен, когда C-библиотека не знает о Python.
Недостаток: копирование может быть затратным для больших списков. Для изменения списка на месте через указатель потребуется обратное копирование.
Пример 4: использование Cython для автоматической генерации C-кода
Cython позволяет писать код, похожий на Python, но компилируемый в C. В нём есть прямой доступ к спискам через индексы.
#!cython
cpdef long sum_cython(list lst):
cdef Py_ssize_t i
cdef long total = 0
for i in range(len(lst)):
total += lst[i]
return total
>>> sum_cython([1,2,3,4]) 10
Пояснения: Cython компилирует код в C-расширение, которое эффективно работает со списками. Это альтернатива ручному API.
Проблема: при использовании Cython необходимо следить за типизацией, иначе может возникнуть дополнительная проверка типов на каждом шаге.