Python C расширения
Интересной возможностью, которую предлагает разработчикам CPython, является простота использования C-кода в Python.
Существует три метода, с помощью которых разработчик может вызвать C функцию из Python кода - ctypes
, SWIG
и Python/C API
. У каждого метода есть свои преимущества и недостатки.
Для начала, зачем нам вообще это может потребоваться?
Несколько популярных причин:
Вам нужна скорость и вы знаете, что C в 50х раз быстрее Python
Вам нужна конкретная C-библиотека и вы не хотите писать "велосипед" на Python
Вам нужен низкоуровневый интерфейс управления ресурсами для работы с памятью
и файлами
Просто потому что Вам так хочется
CTypes
Модуль ctypes один из самых простых способов вызывать C-функции из Python. Он предоставляет C-совместимые типы данных и функции для загрузки DLL, что позволяет обращаться к библиотекам C без их модификации. Отсутствие необходимости изменять C-код объясняет простоту данного метода.
Пример
Простой C-код для суммирования двух чисел, сохраните его как add.c
Теперь скомпилируем C-файл в .so
-файл (DLL под Windows). Так мы получим файл adder.so
.
Теперь Python-код:
Результат:
В примере выше, C-файл содержит простой код - две функции: одна для нахождения суммы двух целых чисел, другая - действительных.
В Python-коде мы сначала импортируем модуль ctypes
. Затем функция CDLL
из того же модуля используется для загрузки C-библиотеки. Теперь функции из C-кода доступны для нас через переменную adder
. Когда мы вызываем adder.add_int()
, то автоматически вызывается C-функция add_int
. Интерфейс модуля ctypes
позволяет использовать питоновские целые числа и строки при вызове C-функций.
Для других типов, например логического или действительных чисел, мы должны использовать корректные ctypes
. Мы делаем это при передаче параметров в adder.add_float()
. Сначала мы создаём требуемый тип c_float
из float
, затем используем в качестве аргумента для C-кода. Этот метод простой и аккуратный, но ограниченный. Мы, к примеру, не можем оперировать объектами на стороне C-кода.
SWIG
Simplified Wrapper and Interface Generator, или SWIG для краткости, это другой способ работы с C-кодом из Python. В этом методе разработчик должен написать отдельный файл, описывающий интерфейс, который будет передаваться в SWIG (утилиту командной строки).
Python-разработчики обычно не используют данный подход, поскольку в большинстве случаев он неоправданно сложен. Тем не менее, это отличный вариант, когда у вас есть C/C++ код, к которому нужно обращаться из множества различных языков.
Пример (с сайта SWIG)
C-код, example.c
содержит различные функции и переменные:
Файл, описывающий интерфейс. Он не будет изменяться в зависимости от языка, на который вы хотите портировать свой C-код:
Компиляция:
Python:
Как мы можем видеть, SWIG позволяет добиваться нужного нам эффекта, но он требует дополнительных усилий, которые, однако, стоит затратить, если вас интересует возможность запуска C-кода из множества различных языков.
Python/C API
C/Python API это, вероятно, наиболее широко применяемый метод - не благодаря своей простоте, а потому что он позволяет оперировать Python объектами из C кода.
Этот метод подразумевает написание C-кода специально для работы с Python. Все объекты Python представляются как структуры PyObject и заголовочный файл Python.h
предоставляет различные функции для работы с объектами. Например, если PyObject
одновременно PyListType
(список), то мы можем использовать функцию PyList_Size()
, чтобы получить длину списка. Это эквивалентно коду len(some_list)
в Python. Большинство основных функций/операторов для стандартных Python объектов доступны в C через Python.h
.
Пример
Давайте напишем С-библиотеку для суммирования всех элементов списка Python (все элементы являются числами).
Начнем с интерфейса, который мы хотим иметь в итоге. Вот Python-файл, использующий пока отсутствующую C-библиотеку:
Смотрится как обыкновенный Python-код, который импортирует и использует Python-модуль addList
. Единственная разница - модуль addList
написан на C.
Дальше на повестке у нас C-код, который будет встроен в Python-модуль addList
, это может смотреться немного странно, однако, разобрав отдельные части, из которых состоит C-файл, вы увидите, что все относительно незатейливо.
adder.c
Пошаговое объяснение:
Заголовочный файл
<Python.h>
содержит все требуемые типы (дляпредставления типов объектов в Python) и определения функций (для работы с
Python-объектами).
Дальше мы пишем функцию, которую собираемся вызывать из Python. По
соглашению, имя функции принимается {module-name}_{function-name}, которое
в нашем случае -
addList_add
. Подробнее об этой функции будет дальше.Затем заполняем таблицу, которая содержит всю необходимую информацию о
функциях, которые мы хотим иметь в модуле. Каждая строка относится к
функции, последняя - контрольное значение (строка из null элементов).
Затем идёт блок инициализации модуля -
PyMODINIT_FUNC init{module-name}
.
Функция addList_add
принимает аргументы типа PyObject
(args также является кортежем, но поскольку в Python всё является объектами, мы используем унифицированный тип PyObject
). Мы парсим входные аргументы (фактически, разбиваем кортеж на отдельные элементы) при помощи PyArg_ParseTuple()
. Первый параметр является аргументом для парсинга. Второй аргумент - строка, регламентирующая процесс парсинга элементов кортежа args. Знак на N-ой позиции строки сообщает нам тип N-ого элемента кортежа args, например - 'i' значит integer, 's' - строка и 'O' - Python-объект. Затем следует несколько аргументов, где мы хотели бы хранить выходные элементы PyArg_ParseTuple()
. Число этих аргументов равно числу аргументов, которые планируется передавать в функцию модуля и их позиционность должна соблюдаться. Например, если мы ожидаем строку, целое число и список в таком порядке, сигнатура функции будет следующего вида:
В данном случае, нам нужно извлечь только объект списка и сохранить его в переменной listObj
. Затем мы используем функцию PyList_Size()
чтобы получить длину списка. Логика совпадает с len(some_list)
в Python.
Теперь мы итерируем по списку, получая элементы при помощи функции PyLint_GetItem(list, index)
. Так мы получаем PyObject*. Однако, поскольку мы знаем, что Python-объекты еще и PyIntType
, то используем функцию PyInt_AsLong(PyObj *)
для получения значения. Выполняем процедуру для каждого элемента и получаем сумму.
Сумма преобразуется в Python-объект и возвращается в Python-код при помощи Py_BuildValue()
. Аргумент "i" означает, что возвращаемое значение имеет тип integer.
В заключение мы собираем C-модуль. Сохраните следующий код как файл setup.py
:
и запустите:
Это соберёт и установит C-файл в Python-модуль, который нам требуется.
Теперь осталось только протестировать работоспособность:
Результат:
В итоге, как вы можете видеть, мы получили наше первое C-расширение, использующее Python.h API. Этот метод может показаться сложным, однако с практикой вы поймёте его удобство.
Из других методов встраивания C-кода в Python, можно отметить альтернативный и быстрый компилятор Cython. Однако Cython, по сути, отличный от основной ветки Python язык, поэтому я не стал здесь его рассматривать.
Last updated