Автор оригинала: Adam McQuistan.
Улучшение Python с помощью пользовательских расширений C
Вступление
В этой статье мы рассмотрим особенности C API CPython, который используется для создания расширений C для Python. Я пройдусь по общему рабочему процессу для того, чтобы взять небольшую библиотеку довольно банальных, игрушечных, например, функций C и выставить ее в оболочке Python.
Возможно, вам интересно… Python-фантастический язык высокого уровня, способный практически на все, зачем мне иметь дело с грязным кодом на языке Си? И я должен был бы согласиться с общей предпосылкой этого аргумента. Тем не менее, есть два распространенных случая использования, которые я нашел, где это, скорее всего, возникнет: (i) для ускорения определенного медленного фрагмента кода Python и (ii) вы вынуждены включить программу, уже написанную на C, в существующую программу Python, и вы не хотите переписывать код C на Python. Последнее случилось со мной недавно, и я хотел поделиться с вами тем, что узнал.
Краткое изложение ключевых шагов
- Получить или написать код на языке Си
- Написать функцию оболочки Python C API
- Таблица определения функций
- Определить модуль
- Функция инициализации записи
- Упакуйте и соберите расширение
Получение или написание кода на языке Си
Для этого урока я буду работать с небольшим набором функций 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, этот учебник вместе с официальными документами поможет вам в достижении этой цели.
Спасибо за чтение, и я приветствую любые комментарии или критические замечания ниже.