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