• en
  • Language: ru
  • Documentation version: latest

22. Расширения Python C

Интересной особенностью, которую предлагает разработчикам реализация CPython, является простота сопряжения кода на языке Си с Python.

Существует три основных метода, используемых разработчиками для вызова функций языка C из кода python - ctypes, SWIG и Python/C API. Каждый метод имеет свои достоинства и недостатки.

Во-первых, зачем вам нужно взаимодействие языка C с Python?

Несколько распространенных причин: :

  • Вам нужна скорость, и вы знаете, что C примерно в 50 раз быстрее, чем Python.

  • Некоторые унаследованные библиотеки на языке C работают так же хорошо, как вы хотите, поэтому не стоит переписывать их на python.

  • Определенный низкоуровневый доступ к ресурсам - от памяти до файловых интерфейсов.

  • Просто потому, что ты этого хочешь.

22.1. CTypes

Python ctypes module - это, вероятно, самый простой способ вызова функций языка Си из Python. Модуль ctypes предоставляет совместимые с Си типы данных и функции для загрузки библиотек DLL, чтобы можно было вызывать общие библиотеки Си без необходимости их модификации. Тот факт, что не нужно трогать сторону Си, добавляет простоты этому методу.

Пример.

Простой код на языке C для сложения двух чисел, сохраните его как add.c

//sample C file to add 2 numbers - int and floats

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.

#For Linux
$  gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c

#For Mac
$ gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c

Теперь в вашем коде python -

from ctypes import *

#load the shared object file
adder = CDLL('./adder.so')

#Find sum of integers
res_int = adder.add_int(4,5)
print "Sum of 4 and 5 = " + str(res_int)

#Find sum of floats
a = c_float(5.5)
b = c_float(4.1)

add_float = adder.add_float
add_float.restype = c_float
print "Sum of 5.5 and 4.1 = ", str(add_float(a, b))

На выходе получаем следующее

Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 =  9.60000038147

В этом примере файл C не требует пояснений - он содержит две функции, одна из которых складывает два целых числа, а другая - два плавающих.

В файле python сначала импортируется модуль ctypes. Затем функция CDLL модуля ctypes используется для загрузки созданного нами общего lib-файла. Функции, определенные в C lib, теперь доступны нам через переменную adder. Когда вызывается adder.add_int(), внутри происходит вызов функции add_int языка Си. Интерфейс ctypes позволяет нам по умолчанию использовать родные целые числа и строки python при вызове функций C.

Для других типов, таких как boolean или float, мы должны использовать правильные ctypes. Это видно при передаче параметров в adder.add_float(). Сначала мы создаем нужные типы c_float из десятичных значений python, а затем используем их в качестве аргументов в коде C. Этот метод прост и чист, но ограничен. Например, невозможно манипулировать объектами на стороне C.

22.2. SWIG

Simplified Wrapper and Interface Generator, или сокращенно SWIG, - это еще один способ сопряжения кода на языке C с Python. В этом методе разработчик должен создать дополнительный файл интерфейса, который является входом для SWIG (утилиты командной строки).

Разработчики Python обычно не используют этот метод, потому что в большинстве случаев он неоправданно сложен. Это отличный метод, когда у вас есть кодовая база на C/C++, и вы хотите сопрячь ее с множеством различных языков.

Пример (из SWIG website )

Код на языке 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(&ltime);
    return ctime(&ltime);
}

Файл интерфейса - он остается неизменным независимо от языка, на который вы хотите перенести ваш C-код:

/* example.i */
 %module example
 %{
 /* Put header files here or function declarations like below */
 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 достигает того же результата, но требует немного больше усилий. Но оно того стоит, если вы ориентируетесь на несколько языков.

22.3. API Python/C

Метод C/Python API, вероятно, является наиболее широко используемым - не из-за его простоты, а из-за того, что вы можете манипулировать объектами python в своем коде на C.

Этот метод требует, чтобы ваш код на языке C был специально написан для взаимодействия с кодом Python. Все объекты Python представлены в виде структуры PyObject, а заголовочный файл Python.h предоставляет различные функции для работы с ней. Например, если PyObject также является PyListType (по сути, списком), то мы можем использовать функцию PyList_Size() на struct для получения длины списка. Это эквивалентно вызову len(list) в python. Большинство базовых функций/операций, которые есть для объектов Python, доступны в C через заголовок Python.h.

Пример.

Написать расширение на языке C, которое добавляет все элементы в список на языке python. (все элементы - числа)

Давайте начнем с конечного интерфейса, который мы хотели бы иметь, вот файл python, использующий расширение C :

#Though it looks like an ordinary python import, the addList module is implemented in C
import addList

l = [1,2,3,4,5]
print "Sum of List - " + str(l) + " = " +  str(addList.add(l))

Приведенное выше выглядит как любой обычный файл python, который импортирует и использует другой модуль python под названием addList. Единственное отличие заключается в том, что модуль addList написан не на Python, а на C.

Далее мы рассмотрим код на языке Си, который встраивается в модуль addList Python. Сначала это может показаться немного сложным, но как только вы поймете различные компоненты, которые используются для написания C-файла, все станет довольно просто.

adder.c

//Python.h has all the required function definitions to manipulate the Python objects
#include <Python.h>

 //This is the function that is called from your python code
static PyObject* addList_add(PyObject* self, PyObject* args){

  PyObject * listObj;

  //The input arguments come as a tuple, we parse the args to get the various variables
  //In this case it's only one list variable, which will now be referenced by listObj
  if (! PyArg_ParseTuple( args, "O", &listObj))
    return NULL;

  //length of the list
  long length = PyList_Size(listObj);

  //iterate over all the elements
  long i, sum =0;
  for(i = 0; i < length; i++){
    //get an element out of the list - the element is also a python objects
    PyObject* temp = PyList_GetItem(listObj, i);
    //we know that object represents an integer - so convert it into C long
    long elem = PyInt_AsLong(temp);
    sum += elem;
  }

  //value returned back to python code - another python object
  //build value here converts the C long to a python integer
  return Py_BuildValue("i", sum);
}

//This is the docstring that corresponds to our 'add' function.
static char addList_docs[] =
    "add( ): add all elements of the list\n";

/* This table contains the relavent info mapping -
  <function-name in python module>, <actual-function>,
  <type-of-args the function expects>, <docstring associated with the function>
*/
static PyMethodDef addList_funcs[] = {
    {"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
    {NULL, NULL, 0, NULL}
};

/*
addList is the module name, and this is the initialization block of the module.
<desired module name>, <the-info-table>, <module's-docstring>
*/
PyMODINIT_FUNC initaddList(void){
    Py_InitModule3("addList", addList_funcs,
                   "Add all ze lists");
}

Пошаговое объяснение :

  • Файл <Python.h> состоит из всех необходимых типов (для представления типов объектов Python) и определений функций (для работы с объектами python).

  • Далее мы напишем функцию, которую планируем вызвать из python. Обычно имена функций имеют вид {module-name}_{function-name}, что в данном случае является addList_add. Подробнее о функции позже.

  • Затем заполните информационную таблицу, которая содержит всю необходимую информацию о функциях, которые мы хотим иметь в модуле. Каждая строка соответствует функции, последняя строка является сентинельным значением (строка нулевых элементов).

  • Наконец, блок инициализации модуля, который имеет сигнатуру PyMODINIT_FUNC init{module-name}.

Функция addList_add принимает аргументы в виде структуры типа PyObject (args также является кортежем - но поскольку все в python является объектом, мы используем общее понятие PyObject). Входящие аргументы разбираются (по сути, разбивают кортеж на отдельные элементы) командой PyArg_ParseTuple(). Первый параметр - это переменная аргумента, которая должна быть разобрана. Второй аргумент - это строка, которая указывает нам, как разобрать каждый элемент кортежа args. Символ в N-й позиции строки указывает нам тип N-го элемента в кортеже args, например, „i“ означает целое число, „s“ означает строку, а „O“ означает объект Python. Далее следуют множественные аргументы, в которых функция PyArg_ParseTuple() будет хранить все разобранные элементы. Количество таких аргументов равно количеству аргументов, которые ожидает получить функция модуля, при этом сохраняется позиционная целостность. Например, если бы мы ожидали получить строку, целое число и список python в таком порядке, сигнатура функции имела бы вид

int n;
char *s;
PyObject* list;
PyArg_ParseTuple(args, "siO", &s, &n, &list);

В этом случае нам нужно только извлечь объект списка и сохранить его в переменной listObj. Затем мы используем функцию PyList_Size() на нашем объекте списка и получаем длину. Это похоже на то, как вы вызываете функцию len(list) в python.

Теперь пройдемся по списку, получим каждый элемент с помощью функции PyList_GetItem(list, index). Это возвращает объект PyObject*. Но поскольку мы знаем, что объекты Python также являются PyIntType, мы просто используем функцию PyInt_AsLong(PyObj *) для получения нужного значения. Мы делаем это для каждого элемента и в итоге получаем сумму.

Сумма преобразуется в объект python и возвращается в код Python с помощью Py_BuildValue(). Здесь «i» указывает на то, что значение, которое мы хотим построить, является целым объектом python.

Теперь мы создадим модуль на языке C. Сохраните следующий код в формате setup.py.

#build the modules

from distutils.core import setup, Extension

setup(name='addList', version='1.0',  \
      ext_modules=[Extension('addList', ['adder.c'])])

и запустить

python setup.py install

Это позволит собрать и установить файл C в нужный нам модуль python.

После всей этой тяжелой работы мы теперь проверим, работает ли модуль.

#module that talks to the C code
import addList

l = [1,2,3,4,5]
print "Sum of List - " + str(l) + " = " +  str(addList.add(l))

И вот вывод

Sum of List - [1, 2, 3, 4, 5] = 15

Итак, как вы видите, мы разработали наше первое успешное расширение C Python, используя API Python.h. Поначалу этот метод кажется сложным, но когда вы привыкнете к нему, он может оказаться весьма полезным.

Другим способом сопряжения C-кода с Python является использование альтернативной и более быстрой сборки python - Cython. Но Cython - это немного другой язык, чем основной поток python, который мы видим. Поэтому этот метод здесь не рассматривается.