Разработка C-расширений для эффективного управления списками Python

Раздел: Продвинутые возможности -> Взаимодействие с C

Способы работы со списками 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 необходимо следить за типизацией, иначе может возникнуть дополнительная проверка типов на каждом шаге.

Использование списков Python в C - comments

En
Python list in c (python)