Расширение возможностей Python внешними C модулями

Раздел: Расширения -> Подключение библиотек

Эффективное решение: использование Cython

Как написать высокопроизводительное расширение на C, сохраняя простоту Python?

Cython позволяет создавать компилируемые модули, которые напрямую вызывают код на C/C++. Он транслирует код на Python-подобном языке в C, после чего компилируется в разделяемую библиотеку (.pyd или .so).

# example.pyx
cdef int add_c(int a, int b):
    return a + b

def add(int a, int b):
    return add_c(a, b)

C python lib (библиотеки c для python)

Файл setup.py для сборки:

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("example.pyx")
)

Сборка выполняется командой python setup.py build_ext --inplace. После этого модуль можно импортировать как обычный Python-модуль. Cython автоматически управляет типами, памятью и обработкой исключений.

Возможные проблемы:

  • Ошибки компиляции из-за несоответствия типов C. Решение: явно указывать типы с помощью cdef.
  • Утечки памяти при работе с динамическими структурами. Использовать cython.view или RAII-обёртки.
  • Проблемы с совместимостью версий Python и Cython. Проверять таблицу совместимости на сайте Cython.

Случаи использования:

  • Ускорение узких мест в численных расчётах (numpy, pandas).
  • Интеграция с существующими библиотеками на C (например, libpng, libxml).
  • Создание обёрток для C++ через cython с использованием cimport.

Как вызвать функции из динамической библиотеки без компиляции?

Решение ctypes - часть стандартной библиотеки Python. Позволяет загружать разделяемые библиотеки (.so, .dll) и вызывать C-функции, описывая их сигнатуры.

import ctypes

lib = ctypes.CDLL("./mylib.so")
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int
result = lib.add(5, 3)
print(result)  # 8

Типичные ошибки:

  • Забытые argtypes и restype приводят к неопределённому поведению и падениям. Всегда указывать прототипы.
  • Неправильное преобразование строк: ctypes использует c_char_p, но строки Python нужно кодировать в байты.
  • Библиотека не найдена - проверять пути и переменную LD_LIBRARY_PATH (Linux) или PATH (Windows).

Случаи использования:

  • Быстрый прототип взаимодействия с любой C-библиотекой (без настройки сборки).
  • Одноразовые скрипты и тесты.

Как безопасно вызывать C-код с автоматической генерацией обвязки?

CFFI (C Foreign Function Interface) предлагает два режима: ABI (in-line) и API (out-of-line). Режим API компилирует C-заглушки, что даёт лучшую производительность и меньше ошибок.

# example_build.py
from cffi import FFI
ffi = FFI()
ffi.cdef("""
    int add(int a, int b);
""")
ffi.set_source("_example",
    """
    int add(int a, int b) {
        return a + b;
    }
    """)
ffi.compile()

# после компиляции импортируем
from _example import ffi, lib
print(lib.add(2, 3))  # 5

Проблемы:

  • Необходимость компилятора C на целевой машине (для режима API).
  • Сложность отладки сегфолтов: в CFFI ошибки маскируются в объект ffi.error.
  • Управление памятью для структур требует явных вызовов ffi.new и ffi.gc.
Как подключить C++ библиотеку с минимальными усилиями?

PyBind11 - современная библиотека для создания расширений Python из C++11. Требует компиляции, но даёт полный контроль над трансформацией типов.

#include <pybind11/pybind11.h>
namespace py = pybind11;

int add(int a, int b) {
    return a + b;
}

PYBIND11_MODULE(example, m) {
    m.def("add", &add, "A function that adds two numbers");
}

Сборка через CMake или pybind11/setup_helpers.py. Результат:

>>> import example
>>> example.add(2, 3)
5

Сложности:

  • Зависимость от C++ компилятора и стандартной библиотеки.
  • Проблемы с перегрузкой функций и аргументами по умолчанию - требуется явная регистрация.
  • Обработка исключений C++ в Python - использовать py::exception.

Случаи использования:

  • Существующий C++ код, который нужно экспортировать в Python.
  • Проекты, где важна производительность и удобство написания расширений на C++.

Расширенные примеры интеграции C и Python

1. Работа с C-структурами через ctypes

Пример
// point.h
typedef struct {
    double x;
    double y;
} Point;

double distance(Point *a, Point *b);

// point.c
#include <math.h>
#include "point.h"
double distance(Point *a, Point *b) {
    double dx = a->x - b->x;
    double dy = a->y - b->y;
    return sqrt(dx*dx + dy*dy);
}

Компиляция библиотеки: gcc -shared -o libpoint.so -fPIC point.c -lm

Пример
# Python код
import ctypes

lib = ctypes.CDLL("./libpoint.so")

class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_double),
                ("y", ctypes.c_double)]

lib.distance.argtypes = [ctypes.POINTER(Point), ctypes.POINTER(Point)]
lib.distance.restype = ctypes.c_double

a = Point(0.0, 0.0)
b = Point(3.0, 4.0)
result = lib.distance(ctypes.byref(a), ctypes.byref(b))
print(f"Расстояние: {result}")  # 5.0
Расстояние: 5.0

Пояснение:

Структура Point объявлена в Python как класс, наследующий ctypes.Structure. Поле _fields_ отображает имена и типы C. Для передачи указателей используется ctypes.byref(). Важно следить за выравниванием: если структура содержит #pragma pack, нужно задать _pack_.

2. Манипуляция массивами в CFFI с возвратом большого объёма данных

Пример
# build.py
from cffi import FFI
ffi = FFI()
ffi.cdef("""
    int* generate_array(int size);
    void free_array(int* arr);
""")
ffi.set_source("_array_example",
    """
    #include <stdlib.h>
    int* generate_array(int size) {
        int* arr = (int*)malloc(size * sizeof(int));
        for(int i=0; i<size; i++) arr[i] = i*i;
        return arr;
    }
    void free_array(int* arr) { free(arr); }
    """)
ffi.compile()

# usage.py
from _array_example import ffi, lib

size = 10
ptr = lib.generate_array(size)
arr = ffi.unpack(ptr, size)  # преобразует в список Python
print(arr[:5])
lib.free_array(ptr)  # обязательно освободить память
[0, 1, 4, 9, 16]

Функция ffi.unpack создаёт временный список. Для работы с огромными массивами удобнее создать ffi.buffer или обертку в numpy с помощью ffi.from_buffer.

3. Использование Cython для вызова C-функции из библиотеки OpenSSL (хеширование)

Пример
# hash.pyx
from libc.stdlib cimport malloc, free
from libc.string cimport memcpy

cdef extern from "openssl/md5.h":
    int MD5(const unsigned char *d, unsigned long n, unsigned char *md)

def md5_hash(bytes data):
    cdef unsigned char[16] result
    cdef unsigned char *buf = data
    MD5(buf, len(data), result)
    return bytes(result[:16])
Пример
# setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("hash.pyx"),
    libraries = ["crypto"]
)
>>> import hash
>>> hash.md5_hash(b"Hello")
b'\x8b\x1a...'

В Cython используется cdef extern для объявления C-функции. Ссылка на библиотеку libcrypto добавляется в setup.py через libraries. Память под результат выделяется на стеке (массив фиксированного размера), что ускоряет выполнение.

4. Передача строк и Callback-функций через PyBind11

Пример
# callback_example.cpp
#include <pybind11/pybind11.h>
#include <pybind11/functional.h>
#include <string>

namespace py = pybind11;

void repeat_twice(const std::string &msg, py::function callback) {
    std::string result = msg + " " + msg;
    callback(result);
}

PYBIND11_MODULE(callback_mod, m) {
    m.def("repeat_twice", &repeat_twice, "Calls callback with repeated string");
}
Пример
# Python
import callback_mod

def my_print(s):
    print("Из C++:", s)

callback_mod.repeat_twice("Привет", my_print)
Из C++: Привет Привет

PyBind11 автоматически преобразует std::string в Python str и py::function в вызываемый объект. Включение заголовка pybind11/functional.h обязательно для поддержки колбэков. Производительность выше, чем через ctypes, но требует компиляции.

5. Прямая работа с Python C API (ручное расширение) – пример модуля со счётчиком

Пример
// counter.c
#include <Python.h>

static PyObject* increment(PyObject *self, PyObject *args) {
    int val;
    if (!PyArg_ParseTuple(args, "i", &val))
        return NULL;
    return PyLong_FromLong(val + 1);
}

static PyMethodDef CounterMethods[] = {
    {"increment", increment, METH_VARARGS, "Increment an integer"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef countermodule = {
    PyModuleDef_HEAD_INIT,
    "counter",
    NULL,
    -1,
    CounterMethods
};

PyMODINIT_FUNC PyInit_counter(void) {
    return PyModule_Create(&countermodule);
}
Пример
# setup.py
from setuptools import setup, Extension

module = Extension('counter', sources=['counter.c'])
setup(name='counter', ext_modules=[module])
>>> import counter
>>> counter.increment(5)
6

Это «классический» способ, требующий ручного управления ссылками и подсчётом объектов. Ошибка в коде C может привести к падению интерпретатора. Используется, когда необходима максимальная интеграция с Python (например, создание собственных типов).

Библиотеки C для Python - comments

En
C python lib (python)