Создание модуля расширения Python C
Оглавление
- Расширение вашей программы на Python
- Написание интерфейса Python на C
- Упаковка вашего модуля расширения Python C
- Создание исключений
- Определение констант
- Тестирование Вашего модуля
- Рассматриваем альтернативные варианты
- Заключение
Существует несколько способов расширить функциональность Python. Один из них - написать свой модуль Python на C или C++. Этот процесс может привести к повышению производительности и улучшению доступа к функциям библиотеки C и системным вызовам. В этом руководстве вы узнаете, как использовать API Python для написания модулей расширения Python C.
Вы узнаете, как:
- Вызывать функции C из Python
- Передавать аргументы из Python в C и анализировать их соответствующим образом
- Вызывать исключения из кода на C и создавать пользовательские исключения на Python на C
- Определить глобальные константы на C и сделать их доступными на Python
- Протестируйте, упакуйте и распространяйте свой модуль расширения Python C
Расширение вашей программы на Python
Одной из менее известных, но невероятно мощных функций Python является его способность вызывать функции и библиотеки, определенные в компилируемых языках , таких как C или C++. Это позволяет вам расширить возможности вашей программы за пределы того, что могут предложить встроенные функции Python.
Существует множество языков, которые вы можете использовать для расширения функциональности Python. Итак, почему вы должны использовать C? Вот несколько причин, по которым вы можете решить создать модуль расширения Python C:
-
Для реализации новых встроенных типов объектов: Можно написать класс Python на C, а затем создать экземпляр и расширить его этот класс из самого Python. Для этого может быть много причин, но чаще всего разработчики обращаются к C в первую очередь из-за производительности. Такая ситуация встречается редко, но полезно знать, до какой степени Python может быть расширен.
-
Для вызова функций библиотеки C и системных вызовов: Многие языки программирования предоставляют интерфейсы для наиболее часто используемых системных вызовов. Тем не менее, могут быть и другие, менее распространенные системные вызовы, доступные только через C. Модуль
osв Python является одним из примеров.
Это не полный список, но он дает вам представление о том, что можно сделать при расширении Python с помощью C или любого другого языка.
Для написания модулей Python на C вам нужно будет использовать Python API, который определяет различные функции, макросы и переменные которые позволяют интерпретатору Python вызывать ваш код на языке Си. Все эти инструменты и многое другое в совокупности в комплекте в Python.h заголовочный файл.
Написание интерфейса Python на C
В этом руководстве вы напишете небольшую оболочку для функции библиотеки C, которую затем вызовете из Python. Самостоятельная реализация оболочки даст вам лучшее представление о том, когда и как использовать C для расширения вашего модуля Python.
Понимание fputs()
fputs() это библиотечная функция C, которую вы будете упаковывать:
int fputs(const char *, FILE *)
Эта функция принимает два аргумента:
const char *представляет собой массив символов.FILE *является указателем файлового потока.
fputs() записывает массив символов в файл, указанный файловым потоком, и возвращает неотрицательное значение. Если операция выполнена успешно, то это значение будет обозначать количество байт, записанных в файл. Если возникает ошибка, то она возвращает EOF. Вы можете прочитать больше об этой библиотечной функции C и других ее вариантах на странице руководства по ..
Написание функции на языке Си для fputs()
Это базовая программа на C, которая использует fputs() для записи строки в поток файлов:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
FILE *fp = fopen("write.txt", "w");
fputs("Real Python!", fp);
fclose(fp);
return 1;
}
Этот фрагмент кода можно резюмировать следующим образом:
- Откройте файл
write.txt. - Запишите строку
"Real Python!"в файл.
Примечание: Приведенный в этой статье код на C должен использоваться в большинстве систем. Он был протестирован на GCC без использования каких-либо специальных флагов.
В следующем разделе вы напишете оболочку для этой функции на языке Си.
Упаковка fputs()
Может показаться немного странным просматривать полный код перед объяснением того, как он работает. Однако, уделив немного времени изучению конечного продукта, вы сможете лучше понять его в следующих разделах. В приведенном ниже блоке кода показана окончательная завершенная версия вашего кода на C:
1#include <Python.h>
2
3static PyObject *method_fputs(PyObject *self, PyObject *args) {
4 char *str, *filename = NULL;
5 int bytes_copied = -1;
6
7 /* Parse arguments */
8 if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
9 return NULL;
10 }
11
12 FILE *fp = fopen(filename, "w");
13 bytes_copied = fputs(str, fp);
14 fclose(fp);
15
16 return PyLong_FromLong(bytes_copied);
17}
Этот фрагмент кода ссылается на три структуры объектов, которые определены в Python.h:
PyObjectPyArg_ParseTuple()PyLong_FromLong()
Они используются для определения типа данных для языка Python. Сейчас вы ознакомитесь с каждым из них.
PyObject
PyObject это объектная структура, используемая для определения типов объектов в Python. Все объекты Python совместно используют небольшое количество полей, которые определяются с помощью структуры PyObject. Все остальные типы объектов являются расширениями этого типа.
PyObject указывает интерпретатору Python обрабатывать указатель на объект как object. Например, установка возвращаемого типа приведенной выше функции как PyObject определяет общие поля, которые требуются интерпретатору Python для распознавания этого типа как допустимого типа Python.
Взгляните еще раз на первые несколько строк вашего кода на C:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Snip */
В строке 2 вы объявляете типы аргументов, которые хотите получить из своего кода на Python:
char *strэто строка, которую вы хотите записать в файловый поток.char *filenameэто имя файла для записи.
PyArg_ParseTuple()
PyArg_ParseTuple() преобразует аргументы, которые вы получите из вашей программы на Python, в локальные переменные:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Parse arguments */
6 if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
7 return NULL;
8 }
9
10 /* Snip */
Если вы посмотрите на строку 6, то увидите, что PyArg_ParseTuple() принимает следующие аргументы:
-
argsотносятся к типуPyObject. -
"ss"это спецификатор формата, который определяет тип данных для анализируемых аргументов. (Вы можете ознакомиться с официальной документацией для получения полной справки.) -
&strи&filenameявляются указателями на локальные переменные, которым будут присвоены проанализированные значения.
PyArg_ParseTuple() в случае сбоя вычисляется значение false. В случае сбоя функция вернет значение NULL и не будет продолжать работу.
fputs()
Как вы уже видели, fputs() принимает два аргумента, одним из которых является объект FILE *. Поскольку вы не можете проанализировать объект Python textIOwrapper с помощью API Python на C, вам придется использовать обходной путь:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Parse arguments */
6 if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
7 return NULL;
8 }
9
10 FILE *fp = fopen(filename, "w");
11 bytes_copied = fputs(str, fp);
12 fclose(fp);
13
14 return PyLong_FromLong(bytes_copied);
15}
Вот краткое описание того, что делает этот код:
- В строке 10 вы передаете имя файла, который вы будете использовать для создания объекта
FILE *и передачи его функции. - В строке 11 вы вызываете
fputs()со следующими аргументами:strэто строка, которую вы хотите записать в файл.fpэтоFILE *объект, который вы определили в строке 10.
Затем вы сохраняете возвращаемое значение fputs() в bytes_copied. Эта целочисленная переменная будет возвращена при вызове fputs() в интерпретаторе Python.
PyLong_FromLong(bytes_copied)
PyLong_FromLong() возвращает PyLongObject, который представляет объект integer в Python. Вы можете найти его в самом конце вашего кода на C:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Parse arguments */
6 if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
7 return NULL;
8 }
9
10 FILE *fp = fopen(filename, "w");
11 bytes_copied = fputs(str, fp);
12 fclose(fp);
13
14 return PyLong_FromLong(bytes_copied);
15}
Строка 14 генерирует PyLongObject для bytes_copied, переменной, которая будет возвращена при вызове функции в Python. Вы должны вернуть PyObject* из вашего модуля расширения Python C обратно в интерпретатор Python.
Запись функции инициализации
Вы написали код, который составляет основную функциональность вашего модуля расширения Python C. Однако, есть еще несколько дополнительных функций, которые необходимы для запуска вашего модуля. Вам нужно будет написать определения вашего модуля и методов, которые он содержит, примерно так:
static PyMethodDef FputsMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef fputsmodule = {
PyModuleDef_HEAD_INIT,
"fputs",
"Python interface for the fputs C library function",
-1,
FputsMethods
};
Эти функции содержат метаинформацию о вашем модуле, которая будет использоваться интерпретатором Python. Давайте рассмотрим каждую из приведенных выше структур, чтобы увидеть, как они работают.
PyMethodDef
Чтобы вызвать методы, определенные в вашем модуле, вам нужно сначала сообщить о них интерпретатору Python. Для этого вы можете использовать PyMethodDef. Это структура с 4 элементами, представляющими один метод в вашем модуле.
В идеале в вашем модуле расширения Python C должно быть несколько методов, которые вы хотите вызывать из интерпретатора Python. Вот почему вам нужно определить массив структур PyMethodDef:
static PyMethodDef FputsMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
{NULL, NULL, 0, NULL}
};
Каждый отдельный элемент структуры содержит следующую информацию:
-
"fputs"это имя, которое пользователь должен был бы написать для вызова этой конкретной функции. -
method_fputsэто имя вызываемой функции на языке Си. -
METH_VARARGSэто флаг, который сообщает интерпретатору, что функция будет принимать два аргумента типаPyObject*:selfявляется объектом модуля.argsэто кортеж, содержащий фактические аргументы вашей функции. Как объяснялось ранее, эти аргументы распаковываются с помощьюPyArg_ParseTuple().
- Последняя строка - это значение, представляющее метод docstring.
PyModuleDef
Точно так же, как PyMethodDef содержит информацию о методах в вашем модуле расширения Python C, структура PyModuleDef содержит информацию о самом вашем модуле. Это не массив структур, а скорее единая структура, которая используется для определения модуля:
static struct PyModuleDef fputsmodule = {
PyModuleDef_HEAD_INIT,
"fputs",
"Python interface for the fputs C library function",
-1,
FputsMethods
};
В этой структуре всего 9 элементов, но не все они обязательны. В приведенном выше блоке кода вы инициализируете следующие пять:
-
PyModuleDef_HEAD_INITявляется членом типаPyModuleDef_Base, которому рекомендуется присвоить только это одно значение. -
"fputs"это название вашего модуля расширения Python C. -
Строка - это значение, представляющее ваш модуль строка документации. Вы можете использовать
NULL, чтобы не указывать строку документации, или вы можете указать строку документации, передавconst char *, как показано в приведенном выше фрагменте. Она имеет типPy_ssize_t. Вы также можете использоватьPyDoc_STRVAR()для определения строки документации для вашего модуля. -
-1это объем памяти, необходимый для хранения состояния вашей программы. Это полезно, когда ваш модуль используется в нескольких подинтерпретаторах, и он может иметь следующие значения:- Отрицательное значение указывает на то, что этот модуль не поддерживает субинтерпретаторы.
- Неотрицательное значение позволяет повторно инициализировать ваш модуль. Оно также определяет объем памяти, который должен выделяться вашему модулю при каждом сеансе работы с субинтерпретатором.
-
FputsMethodsэто ссылка на таблицу вашего метода. Это массив структурPyMethodDef, который вы определили ранее.
Для получения дополнительной информации ознакомьтесь с официальной документацией по Python на PyModuleDef.
PyMODINIT_FUNC
Теперь, когда вы определили свой модуль расширения Python C и структуры методов, пришло время приступить к их использованию. Когда программа на Python импортирует ваш модуль в первый раз, она вызовет PyInit_fputs():
PyMODINIT_FUNC PyInit_fputs(void) {
return PyModule_Create(&fputsmodule);
}
PyMODINIT_FUNC неявно выполняет 3 действия, если указано как тип возвращаемого значения функции:
- Он неявно задает тип возвращаемого значения функции как
PyObject*. - Он объявляет любые специальные связи.
- Он объявляет функцию как extern “C”. В случае, если вы используете C++, он сообщает компилятору C++ не изменять имена символов.
PyModule_Create() вернет новый объект модуля типа PyObject *. В качестве аргумента вы передадите адрес структуры метода, которую вы уже определили ранее, fputsmodule.
Примечание: В Python 3 ваша функция init должна возвращать тип PyObject *. Однако, если вы используете Python 2, то PyMODINIT_FUNC объявляет возвращаемый функцией тип как void.
Собираем все это воедино
Теперь, когда вы написали необходимые части вашего модуля расширения Python C, давайте сделаем шаг назад, чтобы увидеть, как все это сочетается друг с другом. На следующей диаграмме показаны компоненты вашего модуля и то, как они взаимодействуют с интерпретатором Python:
При импорте модуля расширения Python C первым вызываемым методом является PyInit_fputs(). Однако, прежде чем ссылка будет возвращена интерпретатору Python, функция выполняет последующий вызов PyModule_Create(). Это позволит инициализировать структуры PyModuleDef и PyMethodDef, которые содержат метаинформацию о вашем модуле. Имеет смысл подготовить их, поскольку вы будете использовать их в своей функции init.
Как только это будет выполнено, интерпретатору Python будет возвращена ссылка на объект module. На следующей диаграмме показан внутренний поток вашего модуля:
Объект module, возвращаемый PyModule_Create(), содержит ссылку на структуру модуля PyModuleDef, которая, в свою очередь, содержит ссылку на таблицу методов PyMethodDef. Когда вы вызываете метод, определенный в вашем модуле расширения Python C, интерпретатор Python использует объект module и все ссылки, которые он содержит, для выполнения конкретного метода. (Хотя это не совсем то, как интерпретатор Python обрабатывает скрытые возможности, это даст вам представление о том, как это работает.)
Аналогично, вы можете получить доступ к различным другим методам и свойствам вашего модуля, таким как строка документации модуля или строка документации метода. Они определены внутри соответствующих структур.
Теперь у вас есть представление о том, что происходит, когда вы вызываете fputs() из интерпретатора Python. Интерпретатор использует ваш объект module, а также ссылки на модуль и метод для вызова метода. Наконец, давайте взглянем на то, как интерпретатор обрабатывает фактическое выполнение вашего модуля расширения Python C:
После вызова method_fputs() программа выполняет следующие действия:
- Проанализируйте аргументы, которые вы передали из интерпретатора Python, с помощью
PyArg_ParseTuple() - Передайте эти аргументы в
fputs(), библиотечную функцию C, которая формирует суть вашего модуля - Используйте
PyLong_FromLong, чтобы вернуть значение изfputs()
Чтобы увидеть эти же шаги в коде, взгляните на method_fputs() еще раз:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Parse arguments */
6 if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {
7 return NULL;
8 }
9
10 FILE *fp = fopen(filename, "w");
11 bytes_copied = fputs(str, fp);
12 fclose(fp);
13
14 return PyLong_FromLong(bytes_copied);
15}
Итак, ваш метод проанализирует аргументы, переданные вашему модулю, отправит их в fputs() и вернет результаты.
Упаковка вашего модуля расширения Python C
Прежде чем вы сможете импортировать свой новый модуль, вам сначала нужно его собрать. Вы можете сделать это с помощью пакета Python distutils.
Для установки вашего приложения вам понадобится файл с именем setup.py. В этом руководстве вы сосредоточитесь на части, относящейся к модулю расширения Python C. Для получения полного руководства ознакомьтесь с Как опубликовать пакет Python с открытым исходным кодом в PyPI.
Минимальный setup.py файл для вашего модуля должен выглядеть следующим образом:
from distutils.core import setup, Extension
def main():
setup(name="fputs",
version="1.0.0",
description="Python interface for the fputs C library function",
author="<your name>",
author_email="your_email@gmail.com",
ext_modules=[Extension("fputs", ["fputsmodule.c"])])
if __name__ == "__main__":
main()
В приведенном выше блоке кода показаны стандартные аргументы, которые передаются в setup(). Более подробно рассмотрим последний позиционный аргумент, ext_modules. Для этого требуется список объектов класса Extensions. Объект класса Extensions описывает отдельный модуль расширения C или C++ в сценарии установки. Здесь вы передаете два аргумента ключевого слова его конструктору, а именно:
nameэто название модуля.[filename]это список путей к файлам с исходным кодом, относящихся к сценарию установки.
Создание вашего модуля
Теперь, когда у вас есть свой файл setup.py, вы можете использовать его для создания своего модуля расширения Python C. Настоятельно рекомендуется использовать виртуальную среду, чтобы избежать конфликтов с вашей средой Python.
Перейдите в каталог, содержащий setup.py и выполните следующую команду:
$ python3 setup.py install
Эта команда скомпилирует и установит ваш модуль расширения Python C в текущий каталог. Если есть какие-либо ошибки или предупреждения, то ваша программа выдаст их немедленно. Убедитесь, что вы исправили их, прежде чем пытаться импортировать свой модуль.
По умолчанию интерпретатор Python использует clang для компиляции кода на C. Если вы хотите использовать gcc или любой другой компилятор C для выполнения задания, вам необходимо соответствующим образом установить переменную окружения CC либо внутри сценария установки, либо непосредственно в командной строке. Например, вы можете указать интерпретатору Python использовать gcc для компиляции и сборки вашего модуля следующим образом:
$ CC=gcc python3 setup.py install
Однако интерпретатор Python автоматически вернется к gcc, если clang недоступен.
Запуск Вашего модуля
Теперь, когда все готово, пришло время увидеть ваш модуль в действии! Как только он будет успешно собран, запустите интерпретатор для тестового запуска вашего модуля расширения Python C:
>>> import fputs
>>> fputs.__doc__
'Python interface for the fputs C library function'
>>> fputs.__name__
'fputs'
>>> # Write to an empty file named `write.txt`
>>> fputs.fputs("Real Python!", "write.txt")
13
>>> with open("write.txt", "r") as f:
>>> print(f.read())
'Real Python!'
Ваша функция работает так, как ожидалось! Вы передаете строку "Real Python!" и файл, в который нужно записать эту строку, write.txt. Вызов функции fputs() возвращает количество байт, записанных в файл. Вы можете убедиться в этом, распечатав содержимое файла.
Также вспомните, как вы передавали определенные аргументы структурам PyModuleDef и PyMethodDef. Из этого вывода вы можете видеть, что Python использовал эти структуры для присвоения таких параметров, как имя функции и строка документации.
Таким образом, у вас готова базовая версия вашего модуля, но вы можете сделать гораздо больше! Вы можете улучшить свой модуль, добавив такие вещи, как пользовательские исключения и константы.
Создание исключений
Исключения Python сильно отличаются от исключений C++. Если вы хотите вызвать исключения Python из своего модуля расширения C, вы можете использовать для этого API Python. Ниже приведены некоторые функции, предоставляемые Python API для создания исключений:
| Функция | Описание |
|---|---|
PyErr_SetString(PyObject *type,const char *message) |
Принимает два аргумента: аргумент типа PyObject*, указывающий тип исключения, и пользовательское сообщение для отображения пользователю. |
PyErr_Format(PyObject *type,const char *format) |
Принимает два аргумента: аргумент типа PyObject*, указывающий тип исключения, и отформатированное пользовательское сообщение для отображения пользователю. |
PyErr_SetObject(PyObject *type,PyObject *value) |
Принимает два аргумента, оба типа PyObject*: первый указывает тип исключения, а второй задает произвольный объект Python в качестве значения исключения. |
Вы можете использовать любой из них для создания исключения. Однако, какой из них использовать и когда, полностью зависит от ваших требований. В API Python есть все стандартные исключения, предварительно определенные как PyObject типы.
Создание исключений из Кода На C
Хотя вы не можете создавать исключения в C, API Python позволит вам создавать исключения из вашего модуля расширения Python C. Давайте протестируем эту функциональность, добавив PyErr_SetString() в ваш код. Это приведет к возникновению исключения всякий раз, когда длина записываемой строки будет меньше 10 символов:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Parse arguments */
6 if(!PyArg_ParseTuple(args, "ss", &str, &fd)) {
7 return NULL;
8 }
9
10 if (strlen(str) < 10) {
11 PyErr_SetString(PyExc_ValueError, "String length must be greater than 10");
12 return NULL;
13 }
14
15 fp = fopen(filename, "w");
16 bytes_copied = fputs(str, fp);
17 fclose(fp);
18
19 return PyLong_FromLong(bytes_copied);
20}
Здесь вы проверяете длину входной строки сразу после анализа аргументов и перед вызовом fputs(). Если строка, переданная пользователем, короче 10 символов, то ваша программа выдаст ValueError с пользовательским сообщением. Выполнение программы прекращается, как только возникает исключение.
Обратите внимание, что method_fputs() возвращает NULL после создания исключения. Это связано с тем, что всякий раз, когда вы создаете исключение с помощью PyErr_*(), оно автоматически устанавливает внутреннюю запись в таблице исключений и возвращает ее. Вызывающая функция не требуется для последующей повторной установки записи. По этой причине вызывающая функция возвращает значение, указывающее на ошибку, обычно NULL или -1. (Это также должно объяснить, почему возникла необходимость возвращать NULL при анализе аргументов в method_fputs() с помощью PyArg_ParseTuple().)
Создание пользовательских исключений
Вы также можете создавать пользовательские исключения в своем модуле расширения Python C. Однако все немного по-другому. Ранее, в PyMODINIT_FUNC, вы просто возвращали экземпляр, возвращаемый PyModule_Create, и завершали его. Но чтобы ваше пользовательское исключение было доступно пользователю вашего модуля, вам нужно добавить свое пользовательское исключение в экземпляр вашего модуля, прежде чем возвращать его:
static PyObject *StringTooShortError = NULL;
PyMODINIT_FUNC PyInit_fputs(void) {
/* Assign module value */
PyObject *module = PyModule_Create(&fputsmodule);
/* Initialize new exception object */
StringTooShortError = PyErr_NewException("fputs.StringTooShortError", NULL, NULL);
/* Add exception object to your module */
PyModule_AddObject(module, "StringTooShortError", StringTooShortError);
return module;
}
Как и прежде, вы начинаете с создания объекта module. Затем вы создаете новый объект exception, используя PyErr_NewException. В качестве имени класса исключений, который вы хотите создать, используется строка вида module.classname. Выберите что-нибудь описательное, чтобы пользователю было проще понять, что на самом деле пошло не так.
Затем вы добавляете это в свой объект module, используя PyModule_AddObject. В качестве аргументов используется ваш объект module, имя добавляемого нового объекта и сам пользовательский объект exception. Наконец, вы возвращаете свой объект module.
Теперь, когда вы определили пользовательское исключение для вашего модуля, вам нужно обновить method_fputs(), чтобы оно вызывало соответствующее исключение:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Parse arguments */
6 if(!PyArg_ParseTuple(args, "ss", &str, &fd)) {
7 return NULL;
8 }
9
10 if (strlen(str) < 10) {
11 /* Passing custom exception */
12 PyErr_SetString(StringTooShortError, "String length must be greater than 10");
13 return NULL;
14 }
15
16 fp = fopen(filename, "w");
17 bytes_copied = fputs(str, fp);
18 fclose(fp);
19
20 return PyLong_FromLong(bytes_copied);
21}
После создания модуля с новыми изменениями вы можете проверить, работает ли ваше пользовательское исключение должным образом, попытавшись записать строку длиной менее 10 символов:
>>> import fputs
>>> # Custom exception
>>> fputs.fputs("RP!", fp.fileno())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
fputs.StringTooShortError: String length must be greater than 10
При попытке ввести строку, содержащую менее 10 символов, генерируется пользовательское исключение с сообщением, объясняющим, что пошло не так.
Определение констант
В некоторых случаях вам потребуется использовать или определять константы в вашем модуле расширения Python C. Это очень похоже на то, как вы определяли пользовательские исключения в предыдущем разделе. Вы можете определить новую константу и добавить ее в свой экземпляр модуля, используя PyModule_AddIntConstant():
PyMODINIT_FUNC PyInit_fputs(void) {
/* Assign module value */
PyObject *module = PyModule_Create(&fputsmodule);
/* Add int constant by name */
PyModule_AddIntConstant(module, "FPUTS_FLAG", 64);
/* Define int macro */
#define FPUTS_MACRO 256
/* Add macro to module */
PyModule_AddIntMacro(module, FPUTS_MACRO);
return module;
}
Эта функция API Python принимает следующие аргументы:
- Экземпляр вашего модуля
- Имя константы
- Значение константы
Вы можете сделать то же самое для макросов, используя PyModule_AddIntMacro():
PyMODINIT_FUNC PyInit_fputs(void) {
/* Assign module value */
PyObject *module = PyModule_Create(&fputsmodule);
/* Add int constant by name */
PyModule_AddIntConstant(module, "FPUTS_FLAG", 64);
/* Define int macro */
#define FPUTS_MACRO 256
/* Add macro to module */
PyModule_AddIntMacro(module, FPUTS_MACRO);
return module;
}
Эта функция принимает следующие аргументы:
- Экземпляр вашего модуля
- Имя макроса, который уже был определен
Примечание: Если вы хотите добавить строковые константы или макросы в свой модуль, то вы можете использовать PyModule_AddStringConstant() и PyModule_AddStringMacro() соответственно.
Откройте интерпретатор Python, чтобы проверить, работают ли ваши константы и макросы должным образом:
>>> import fputs
>>> # Constants
>>> fputs.FPUTS_FLAG
64
>>> fputs.FPUTS_MACRO
256
Здесь вы можете видеть, что константы доступны из интерпретатора Python.
Тестирование Вашего модуля
Вы можете протестировать свой модуль расширения Python C точно так же, как и любой другой модуль Python. Это можно продемонстрировать, написав небольшую тестовую функцию для pytest:
import fputs
def test_copy_data():
content_to_copy = "Real Python!"
bytes_copied = fputs.fputs(content_to_copy, 'test_write.txt')
with open('test_write.txt', 'r') as f:
content_copied = f.read()
assert content_copied == content_to_copy
В приведенном выше тестовом сценарии вы используете fputs.fputs() для записи строки "Real Python!" в пустой файл с именем test_write.txt. Затем вы читаете содержимое этого файла и используете оператор assert, чтобы сравнить его с тем, что вы написали изначально.
Вы можете запустить этот набор тестов, чтобы убедиться, что ваш модуль работает должным образом:
$ pytest -q
test_fputs.py [100%]
1 passed in 0.03 seconds
Для более подробного ознакомления ознакомьтесь с Начало тестирования на Python.
Рассматриваем альтернативные варианты
В этом руководстве вы создали интерфейс для библиотечной функции C, чтобы понять, как писать модули расширения Python C. Однако бывают случаи, когда все, что вам нужно сделать, это вызвать несколько системных вызовов или несколько функций библиотеки C, и вы хотите избежать накладных расходов, связанных с написанием на двух разных языках. В этих случаях вы можете использовать библиотеки Python, такие как ctypes или cffi.
Это Библиотеки внешних функций для Python, которые предоставляют доступ к функциям и типам данных библиотеки C. Хотя в сообществе нет единого мнения о том, какая библиотека лучше, у обеих есть свои преимущества и недостатки. Другими словами, любой из них был бы хорошим выбором для любого конкретного проекта, но есть несколько вещей, которые следует иметь в виду, когда вам нужно выбирать между ними:
-
Библиотека
ctypesвходит в состав стандартной библиотеки Python. Это очень важно, если вы хотите избежать внешних зависимостей. Это позволяет вам писать на Python оболочки для других языков. -
Библиотека
cffiеще не включена в стандартную библиотеку. Это может привести к нарушению условий для вашего конкретного проекта. В целом, это более питоническое по своей природе, но оно не справляется с предварительной обработкой за вас.
Для получения дополнительной информации об этих библиотеках ознакомьтесь с Расширением Python с помощью библиотек C и модуля “ctypes” и Взаимодействие Python и C: модуль CFFI.
Примечание: Помимо ctypes и cffi, существуют различные другие доступные инструменты. Например, вы также можете использовать swig и boost::Py.
Заключение
В этом руководстве вы узнали, как написать интерфейс Python на языке программирования C, используя Python API. Вы написали оболочку на Python для функции библиотеки fputs() C. Вы также добавили пользовательские исключения и константы в свой модуль перед его сборкой и тестированием.
Python API предоставляет множество функций для написания сложных интерфейсов Python на языке программирования C. В то же время такие библиотеки, как cffi или ctypes, могут снизить количество накладных расходов, связанных с написанием модулей расширения Python C. Убедитесь, что вы взвесили все факторы, прежде чем принимать решение!


