Интересной возможностью, которую предлагает разработчикам CPython, является простота использования C-кода в Python.
Существует три метода, с помощью которых разработчик может вызвать C функцию
из Python кода - ctypes
, SWIG
и Python/C API
. У каждого метода
есть свои преимущества и недостатки.
Для начала, зачем нам вообще это может потребоваться?
Несколько популярных причин:
- Вам нужна скорость и вы знаете, что C в 50х раз быстрее Python
- Вам нужна конкретная C-библиотека и вы не хотите писать "велосипед" на Python
- Вам нужен низкоуровневый интерфейс управления ресурсами для работы с памятью и файлами
- Просто потому что Вам так хочется
Модуль ctypes один из самых простых способов вызывать C-функции из Python. Он предоставляет C-совместимые типы данных и функции для загрузки DLL, что позволяет обращаться к библиотекам C без их модификации. Отсутствие необходимости изменять C-код объясняет простоту данного метода.
Пример
Простой C-код для суммирования двух чисел, сохраните его как add.c
// Простой C-файл - суммируем целые и действительные числа
#include <stdio.h>
int add_int(int, int);
float add_float(float, float);
int add_int(int num1, int num2){
return num1 + num2;
}
float add_float(float num1, float num2){
return num1 + num2;
}
Теперь скомпилируем C-файл в .so
-файл (DLL под Windows). Так мы получим
файл adder.so
.
# Для Linux
$ gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c
# Для Mac
$ gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c
Теперь Python-код:
from ctypes import *
# Загружаем библиотеку
adder = CDLL('./adder.so')
# Находим сумму целых чисел
res_int = adder.add_int(4,5)
print("Сумма 4 и 5 = " + str(res_int))
# Находим сумму действительных чисел
a = c_float(5.5)
b = c_float(4.1)
add_float = adder.add_float
add_float.restype = c_float
print("Сумма 5.5 и 4.1 = " + str(add_float(a, b)))
Результат:
Сумма 4 и 5 = 9
Сумма 5.5 и 4.1 = 9.60000038147
В примере выше, C-файл содержит простой код - две функции: одна для нахождения суммы двух целых чисел, другая - действительных.
В Python-коде мы сначала импортируем модуль ctypes
. Затем функция CDLL
из того же модуля используется для загрузки C-библиотеки. Теперь
функции из C-кода доступны для нас через переменную adder
. Когда мы
вызываем adder.add_int()
, то автоматически вызывается C-функция
add_int
. Интерфейс модуля ctypes
позволяет использовать питоновские
целые числа и строки при вызове C-функций.
Для других типов, например логического или действительных чисел, мы должны
использовать корректные ctypes
. Мы делаем это при передаче параметров в
adder.add_float()
. Сначала мы создаём требуемый тип c_float
из float
,
затем используем в качестве аргумента для C-кода. Этот метод простой и
аккуратный, но ограниченный. Мы, к примеру, не можем оперировать объектами на
стороне C-кода.
Simplified Wrapper and Interface Generator, или SWIG для краткости, это другой способ работы с C-кодом из Python. В этом методе разработчик должен написать отдельный файл, описывающий интерфейс, который будет передаваться в SWIG (утилиту командной строки).
Python-разработчики обычно не используют данный подход, поскольку в большинстве случаев он неоправданно сложен. Тем не менее, это отличный вариант, когда у вас есть C/C++ код, к которому нужно обращаться из множества различных языков.
Пример (с сайта SWIG)
C-код, example.c
содержит различные функции и переменные:
#include <time.h>
double My_variable = 3.0;
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
int my_mod(int x, int y) {
return (x%y);
}
char *get_time()
{
time_t ltime;
time(<ime);
return ctime(<ime);
}
Файл, описывающий интерфейс. Он не будет изменяться в зависимости от языка, на который вы хотите портировать свой C-код:
/* example.i */
%module example
%{
/* Помещаем сюда заголовочные файлы или объявления функций */
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
%}
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
Компиляция:
unix % swig -python example.i
unix % gcc -c example.c example_wrap.c \
-I/usr/local/include/python2.1
unix % ld -shared example.o example_wrap.o -o _example.so
Python:
>>> import example
>>> example.fact(5)
120
>>> example.my_mod(7,3)
1
>>> example.get_time()
'Sun Feb 11 23:01:07 1996'
>>>
Как мы можем видеть, SWIG позволяет добиваться нужного нам эффекта, но он требует дополнительных усилий, которые, однако, стоит затратить, если вас интересует возможность запуска C-кода из множества различных языков.
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 import, addList это C-библиотека
import addList
l = [1,2,3,4,5]
print("Сумма элементов списка - " + str(l) + " = " + str(addList.add(l)))
Смотрится как обыкновенный Python-код, который импортирует и использует
Python-модуль addList
. Единственная разница - модуль addList
написан
на C.
Дальше на повестке у нас C-код, который будет встроен в Python-модуль
addList
, это может смотреться немного странно, однако, разобрав отдельные
части, из которых состоит C-файл, вы увидите, что все относительно
незатейливо.
adder.c
// Python.h содержит все необходимые функции, для работы с объектами Python
#include <Python.h>
// Эту функцию мы вызываем из Python кода
static PyObject* addList_add(PyObject* self, PyObject* args){
PyObject * listObj;
// Входящие аргументы находятся в кортеже
// В нашем случае есть только один аргумент - список, на который мы будем
// ссылаться как listObj
if (! PyArg_ParseTuple( args, "O", &listObj))
return NULL;
// Длина списка
long length = PyList_Size(listObj);
// Проходимся по всем элементам
long i, sum =0;
for(i = 0; i < length; i++){
// Получаем элемент из списка - он также Python-объект
PyObject* temp = PyList_GetItem(listObj, i);
// Мы знаем, что элемент это целое число - приводим его к типу C long
long elem = PyInt_AsLong(temp);
sum += elem;
}
// Возвращаемое в Python-код значение также Python-объект
// Приводим C long к Python integer
return Py_BuildValue("i", sum);
}
// Немного документации для `add`
static char addList_docs[] =
"add( ): add all elements of the list\n";
/*
Эта таблица содержит необходимую информацию о функциях модуля
<имя функции в модуле Python>, <фактическая функция>,
<ожидаемые типы аргументов функции>, <документация функции>
*/
static PyMethodDef addList_funcs[] = {
{"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
{NULL, NULL, 0, NULL}
};
/*
addList имя модуля и это блок его инициализации.
<желаемое имя модуля>, <таблица информации>, <документация модуля>
*/
PyMODINIT_FUNC initaddList(void){
Py_InitModule3("addList", addList_funcs,
"Add all ze lists");
}
Пошаговое объяснение:
- Заголовочный файл
<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()
.
Число этих аргументов равно числу аргументов, которые планируется передавать
в функцию модуля и их позиционность должна соблюдаться. Например, если мы
ожидаем строку, целое число и список в таком порядке, сигнатура функции будет
следующего вида:
int n;
char *s;
PyObject* list;
PyArg_ParseTuple(args, "siO", &s, &n, &list);
В данном случае, нам нужно извлечь только объект списка и сохранить его в
переменной 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
:
# Собираем модули
from distutils.core import setup, Extension
setup(name='addList', version='1.0',\
ext_modules=[Extension('addList', ['adder.c'])])
и запустите:
python setup.py install
Это соберёт и установит C-файл в Python-модуль, который нам требуется.
Теперь осталось только протестировать работоспособность:
# Модуль, вызывающий C-код
import addList
l = [1,2,3,4,5]
print("Сумма элементов списка - " + str(l) + " = " + str(addList.add(l)))
Результат:
Сумма элементов списка - [1, 2, 3, 4, 5] = 15
В итоге, как вы можете видеть, мы получили наше первое C-расширение, использующее Python.h API. Этот метод может показаться сложным, однако с практикой вы поймёте его удобство.
Из других методов встраивания C-кода в Python, можно отметить альтернативный и быстрый компилятор Cython. Однако Cython, по сути, отличный от основной ветки Python язык, поэтому я не стал здесь его рассматривать.