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

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

Вступление

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

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

Резюме ключевых шагов

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

Получение или написание кода C

В этом руководстве я буду работать с небольшим набором функций C, которые я написал с ограниченными знаниями 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 <stdio.h> 
 #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.h - это файл заголовка C, который определяет сигнатуры функций, с которыми я буду работать, а второй файл demolib.c показывает фактические реализации этих функций.

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

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

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

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

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

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

 // demomodule.c 
 #include <Python.h> 
 #include "demolib.h" 

Теперь я начну работать над определением оболочки для первой функции C cfactorial_sum(...) . Функция должна быть статической, поскольку ее область действия должна быть ограничена только этим файлом, и она должна возвращать PyObject доступный для нашей программы через файл заголовка Python.h. Имя функции-оболочки будет DemoLib_cFactorialSum и она будет содержать два аргумента, оба типа PyObject первый из которых является указателем на себя, а второй - указателем на аргументы, переданные функции через вызывающий код 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 обнаруживается ошибка, я возвращаю 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 Описание


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

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

Хорошо, теперь, когда мы получили представление о том, как работает PyArg_ParseTuple(...) я продолжу. Следующее, что нужно сделать, - это вызвать cfactorial_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_cFactorialSum(...) - это вернуть сумму в форме, с которой может работать клиентский код Python. Для этого я использую другой инструмент под названием Py_BuildValue(...) доступный через сокровищницу Python.h. Py_BuildValue использует спецификаторы формата, очень похожие на то, как PyArg_ParseTuple(...) использует их, только в противоположном направлении. Py_BuildValue также позволяет возвращать наши знакомые структуры данных Python, такие как кортежи и словари. В этой функции-оболочке я верну в Python int, который я реализую следующим образом:

 static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) { 
 // arg parsing omitted 
 
 // factorial function call omitted 
 
 return Py_BuildValue("i", fact_sum); 
 } 

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

Код оболочки Вернулся на Python


Py_BuildValue ("s", "A") "А" Py_BuildValue ("я", 10) 10 Py_BuildValue ("(iii)", 1, 2, 3) (1, 2, 3) Py_BuildValue ("{си, си}", "а ', 4," б ", 9) {"a": 4, "b": 9} Py_BuildValue ("") Никто

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

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

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

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

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

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

Теперь, когда я знаю размер списка, я могу преобразовать его элементы в массив целых ifactorial_sum в мою функцию ifactorial_sum C, которая была определена ранее. Для этого я использую цикл for для перебора элементов списка, извлекая каждый элемент с помощью PyList_GetItem , который возвращает PyObject реализованный как представление Python длинного PyLongObject . Затем я использую PyLong_AsLong чтобы преобразовать представление 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; 
 } 

На этом этапе я могу вызвать свою ifactorial_sum(...) передав ей nums и n , которая возвращает факториальную сумму массива long. Опять же, я использую Py_BuildValue чтобы преобразовать сумму обратно в Python int и вернуть ее в код Python вызывающего клиента.

 unsigned long fact_sum; 
 fact_sum = ifactorial_sum(nums, n); 
 
 return Py_BuildValue("i", fact_sum); 

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

Определить таблицу функций

В этом разделе я напишу массив, который связывает две функции-оболочки, написанные в предыдущем разделе, с именем, которое будет отображаться в 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_FunctionsTable с модулем. Эта структура также отвечает за определение имени модуля, представленного в Python, а также за предоставление строки документа уровня модуля.

 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 
 }; 

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

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

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

Полный код расширения теперь выглядит так:

 #include <stdio.h> 
 #include <Python.h> 
 #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 и функцию настройки из 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 
 >>> 

Заключение

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

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus