Рубрики
Без рубрики

Улучшение Python с помощью пользовательских расширений C

Автор оригинала: Adam McQuistan.

Улучшение Python с помощью пользовательских расширений C

Вступление

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

Возможно, вам интересно… Python-фантастический язык высокого уровня, способный практически на все, зачем мне иметь дело с грязным кодом на языке Си? И я должен был бы согласиться с общей предпосылкой этого аргумента. Тем не менее, есть два распространенных случая использования, которые я нашел, где это, скорее всего, возникнет: (i) для ускорения определенного медленного фрагмента кода Python и (ii) вы вынуждены включить программу, уже написанную на C, в существующую программу Python, и вы не хотите переписывать код C на Python. Последнее случилось со мной недавно, и я хотел поделиться с вами тем, что узнал.

Краткое изложение ключевых шагов

  1. Получить или написать код на языке Си
  2. Написать функцию оболочки Python C API
  3. Таблица определения функций
  4. Определить модуль
  5. Функция инициализации записи
  6. Упакуйте и соберите расширение

Получение или написание кода на языке Си

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

// demolib.h
unsigned long cfactorial_sum(char num_chars[]);
unsigned long ifactorial_sum(long nums[], int size);
unsigned long factorial(long n);

#include 
#include "demolib.h"

unsigned long cfactorial_sum(char num_chars[]) {
    unsigned long fact_num;
    unsigned long sum = 0;

    for (int i = 0; num_chars[i]; i++) {
        int ith_num = num_chars[i] - '0';
        fact_num = factorial(ith_num);
        sum = sum + fact_num;
    }
    return sum;
}

unsigned long ifactorial_sum(long nums[], int size) {
    unsigned long fact_num;
    unsigned long sum = 0;
    for (int i = 0; i < size; i++) {
        fact_num = factorial(nums[i]);
        sum += fact_num;
    }
    return sum;
}

unsigned long factorial(long n) {
    if (n == 0)
        return 1;
    return (unsigned)n * factorial(n-1);
}

Первый файл demolib.his является заголовочным файлом C, который определяет сигнатуры функций, с которыми я буду работать, а второй файл demolib.c показывает фактические реализации этих функций.

Первая функция factorial_sum(char num_chars[]) получает строку C числовых цифр, представленную массивом символов, где каждый символ является числом. Функция строит сумму путем циклического перебора каждого символа, преобразования его в int, вычисления факториала этого int через factorial(long n) и добавления его к кумулятивной сумме. Наконец, он возвращает сумму вызывающему его клиентскому коду.

Вторая функция factorial_sum(long nums[], int size) ведет себя аналогично factorial_sum(...) , но без необходимости преобразования в ints.

Последняя функция является простой факториальной(long n) функцией, реализованной в алгоритме рекурсивного типа.

Написание функций оболочки Python C API

Написание функции-оболочки C to Python-самая сложная часть всего процесса, который я собираюсь продемонстрировать. API расширения Python C, который я буду использовать, находится в заголовочном файле Python.h, который поставляется в комплекте с большинством установок Python. Для целей этого урока я буду использовать дистрибутив anaconda CPython 3.6.

Во – первых, я включу Питона.h заголовочный файл в верхней части нового файла под названием demomodule.c, и я также включу свой пользовательский заголовочный файл demolib.h, поскольку он как бы служит интерфейсом для функций, которые я буду оборачивать. Я также должен добавить, что все файлы, с которыми мы работаем, должны находиться в одном каталоге.

// demomodule.c
#include 
#include "demolib.h"

Теперь я начну работать над определением оболочки для первой функции C factorial_sum(...) . Функция должна быть статичной, так как ее область действия должна быть ограничена только этим файлом, и она должна возвращать PyObject , открытый нашей программе через Python.h заголовочный файл. Имя функции-оболочки будет DemoLib_cFactorialSum и будет содержать два аргумента, оба типа PyObject , первый из которых является указателем на self, а второй-указателем на args, передаваемые функции через вызывающий код Python.

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    ...
}

Далее мне нужно разобрать строку цифр, которую клиентский код Python будет передавать этой функции, и преобразовать ее в массив символов C, чтобы она могла быть использована функцией cfactorial_sum(...) для возврата факториальной суммы. Я сделаю это с помощью PyArg_ParseTuple(...) .

Сначала мне нужно будет определить указатель C char с именем char_nums , который будет получать содержимое строки Python, передаваемой функции. Дальше я позвоню PyArg_ParseTuple(...) передает ему значение PyObject args, строку формата "s" , которая указывает, что первым (и единственным) параметром args является строка, которая должна быть принудительно введена в последний аргумент, переменную char_nums .

Если в возникает ошибка PyArg_ParseTuple(...) он вызовет соответствующее исключение ошибки типа, и возвращаемое значение будет равно нулю, что интерпретируется как false в условном выражении. Если в моем if-операторе обнаружена ошибка , я возвращаю a NULL , который сигнализирует вызывающему коду Python, что произошло исключение.

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &char_nums)) {
        return NULL:
    }
}

Я хотел бы немного поговорить о том, как работает функция PyArg_ParseTuple (...) . Я построил ментальную модель вокруг функции таким образом, что вижу ее как принимающую переменное число позиционных аргументов, переданных клиентской функции Python и захваченных параметром PyObject *args . Затем я думаю о аргументах, захваченных параметром *args , как о распакованных в C-определенные переменные, которые идут после спецификатора строки формата.

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

обуглить c Строка Python длиной 1 преобразуется в C char
массив символов s Строка Python преобразуется в массив символов C
двойной d Python float преобразуется в C double
плыть f Python float преобразуется в C float
инт i Python int преобразован в C int
длинный l Python int преобразуется в C long
PyObject * o Объект Python преобразуется в C PyObject

Если вы передаете несколько аргументов функции, которые должны быть распакованы и принудительно преобразованы в типы C, то вы просто используете несколько спецификаторов, таких как PyArg_ParseTuple(args, "si", &charVar, &IntVar) .

Хорошо, теперь, когда у нас есть ощущение того, как PyArg_ParseTuple(...) работы я буду продвигать вперед. Следующее, что нужно сделать, – это вызвать функцию factorial_sum (...) , передав ей массив char_nums , который мы только что построили из строки Python, переданной в оболочку. Возврат будет беззнаковым длинным.

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted
    unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);
}

Последнее, что нужно сделать в функции-оболочке DemoLib_c Factorial Sum (...) , – это вернуть сумму в форме, с которой может работать клиентский код Python. Для этого я использую другой инструмент под названием Py_BuildValue(...) exposed через сокровищницу Python.h. Py_BuildValue использует спецификаторы формата, очень похожие на то, как PyArg_ParseTuple(...) использует их, только в противоположном направлении. Py_BuildValue также позволяет возвращать наши знакомые структуры данных Python, такие как кортежи и дикты. В этой функции-оболочке я буду возвращать int в Python, который я реализую следующим образом:

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted

    // factorial function call omitted

    return Py_BuildValue("i", fact_sum);
}

Вот некоторые примеры некоторых других форматов и типов возвращаемых значений:

Py_BuildValue(“s”, “A”) “A”
Значение Py_Build(“i”, 10) 10
Значение Py_Build(“(iii)”, 1, 2, 3) (1, 2, 3)
Значение Py_Build(“{si,si}”, “a’, 4, “b”, 9) {“а”: 4, “б”: 9}
Значение Py_Build(“”) Никто

Круто, правда?!

Теперь давайте перейдем к реализации оболочки для другой функции C factorial_sum(...) . Эта обертка будет включать в себя несколько других причуд, которые нужно проработать.

static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {
    PyObject *lst;
    if(!PyArg_ParseTuple(args, "O", &lst)) {
        return NULL;
    }
}

Как вы можете видеть, сигнатура функции такая же, как и в предыдущем примере , поскольку она статична, возвращает PyObject , а параметры-два PyObjects . Однако разбор аргументов немного отличается. Поскольку функция Python будет передана списку, который не имеет узнаваемого типа C, мне нужно использовать больше инструментов Python C API. Спецификатор формата “O” в PyArg_ParseTuple указывает, что ожидается PyObject , который присваивается универсальной переменной PyObject *lst .

За кулисами механизм Python C API распознает, что переданный аргумент реализует интерфейс последовательности, который позволяет мне получить размер переданного списка с помощью функции PyObject_Length . Если этой функции задан тип PyObject , который не реализует интерфейс последовательности, то возвращается a NULL .

    int n = PyObject_Length(lst);
    if (n < 0) {
        return NULL;
    }

Теперь, когда я знаю размер списка, я могу преобразовать его элементы в массив C ints и передать его в мой ifactorial_sum Функция C, которая была определена ранее. Для этого я использую цикл for для итерации по элементам списка, извлекая каждый элемент с помощью PyList_GetItem , который возвращает PyObject , реализованный как Python-представление длинного вызываемого PyLongObject . Затем я использую Pylons_As Long для преобразования представления Python long в общий тип данных C long и заполнения массива C long, который я назвал nums .

  long nums[n];
  for (int i = 0; i < n; i++) {
    PyLongObject *item = PyList_GetItem(lst, i);
    long num = PyLong_AsLong(item);
    nums[i] = num;
  }

В этот момент я могу вызвать свою factorial_sum(...) функцию , передающую ее numbs и n , которая возвращает факториальную сумму массива длинных. Опять же, я буду использовать Py_Build Value для преобразования суммы обратно в Python int и возврата ее в вызывающий клиентский код Python.

    unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);

    return Py_BuildValue("i", fact_sum);

Остальная часть кода, который будет написан, – это просто шаблонный код Python C API, который я потрачу меньше времени на объяснение и отсылаю читателя к docs для получения более подробной информации.

Таблица определения функций

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

static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum",      // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS,          // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum",      // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS,          // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};

Определить модуль

Здесь я приведу определение модуля, которое связывает ранее определенную таблицу DemoLib_Functions/| массив с модулем. Эта структура также отвечает за определение имени модуля, который представлен в Python, а также за предоставление строки doc уровня модуля.

static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};

Напишите функцию инициализации

Последний желаемый бит кода для записи-это функция инициализации модуля, которая является единственным нестатическим членом кода-оболочки. Эта функция имеет очень специфическое соглашение об именовании PyInit_name , где name – это имя модуля. Эта функция вызывается в интерпретаторе Python, который создает модуль и делает его доступным.

PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&DemoLib_Module);
}

Полный код расширения теперь выглядит следующим образом:

#include 
#include 
#include "demolib.h"

// wrapper function for cfactorial_sum
static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &char_nums)) {
        return NULL;
    }

    unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);

    return Py_BuildValue("i", fact_sum);
}

// wrapper function for ifactorial_sum
static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {
    PyObject *lst;
    if (!PyArg_ParseTuple(args, "O", &lst)) {
        return NULL;
    }

    int n = PyObject_Length(lst);
    if (n < 0) {
        return NULL;
    }

    long nums[n];
    for (int i = 0; i < n; i++) {
        PyLongObject *item = PyList_GetItem(lst, i);
        long num = PyLong_AsLong(item);
        nums[i] = num;
    }

    unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);

    return Py_BuildValue("i", fact_sum);
}

// module's function table
static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum", // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum", // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};

// modules definition
static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};

PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&DemoLib_Module);
}

Упаковка и построение расширения

Теперь я упакую и соберу расширение, чтобы использовать его в Python с помощью библиотеки setuptools .

Первое, что мне нужно будет сделать, это установить setuptools:

$ pip install setuptools

Теперь я создам новый файл под названием setup.py. Ниже приведено представление о том, как организованы мои файлы:

├── demolib.c
├── demolib.h
├── demomodule.c
└── setup.py

Внутри setup.py поместите следующий код, который импортирует класс Extension и функцию setup из setuptools. Я создаю экземпляр класса Extension , который используется для компиляции кода C с помощью компилятора gcc , который изначально установлен в большинстве операционных систем Unix-стиля. Пользователи Windows захотят установить MinGW .

Последний показанный бит кода просто передает минимальную предложенную информацию для упаковки кода в пакет Python.

from setuptools import Extension, setup

module = Extension("demo",
                  sources=[
                    'demolib.c',
                    'demomodule.c'
                  ])
setup(name='demo',
     version='1.0',
     description='Python wrapper for custom C extension',
     ext_modules=[module])

В оболочке я выполню следующую команду для сборки и установки пакета в мою систему. Этот код определит местонахождение setup.py файл и вызовите его setup(...) функцию:

$ pip install .

Наконец, теперь я могу запустить интерпретатор Python, импортировать свой модуль и протестировать свои функции расширения:

$  python
Python 3.6.4 |Anaconda, Inc.| (default, Dec 21 2017, 15:39:08)
>>> import demo
>>> demo.sfactorial_sum("12345")
153
>>> demo.ifactorial_sum([1,2,3,4,5])
153
>>>

Вывод

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

Спасибо за чтение, и я приветствую любые комментарии или критические замечания ниже.