Оригинал статьи: Building a Python C Extension Module
Есть несколько способов расширить функциональность Python. Одним из них является написание собственного модуля на C или C++. Этот процесс может привести к повышению производительности и улучшению доступа к функциям библиотеки C и системным вызовам. В этом руководстве вы узнаете, как использовать API Python для написания модулей расширения Python C.
Вы узнаете, как:
- Вызывать функции C из Python.
- Передавать аргументы из Python в C и анализировать их.
- Вызывать исключения из кода C и создавать собственные исключения Python в C.
- Определять глобальные константы в C и делать их доступными в Python.
Расширение вашей программы Python
Есть много языков, из которых вы можете выбрать для расширения функциональности Python. Итак, почему вы должны использовать C? Вот несколько причин, почему вы можете решить создать модуль расширения на C:
-
Для реализации новых встроенных типов объектов: можно написать класс Python на C, а затем создать и расширить этот класс из самого Python. Для этого может быть много причин, но чаще всего именно производительность побуждает разработчиков обращаться к C. Такая ситуация встречается редко, но хорошо знать, в какой степени Python может быть расширен.
-
Для вызова функций библиотеки C и системных вызовов: Многие языки программирования предоставляют интерфейсы для наиболее часто используемых системных вызовов. Тем не менее, могут быть другие менее используемые системные вызовы, которые доступны только через C. Модуль
os
в Python является одним из примеров.
Это не исчерпывающий список, но он дает вам представление о том, что можно сделать при расширении Python с использованием C или любого другого языка.
Чтобы писать модули Python на C, вам нужно использовать API Python, который определяет различные функции, макросы и переменные, которые позволяют интерпретатору Python вызывать ваш код на C. Все эти и другие инструменты собраны в заголовочном файле Python.h.
Написание интерфейса Python на C
В этом руководстве вы напишете небольшую оболочку для функции библиотеки C, которую затем будете вызывать из Python. Реализация оболочки самостоятельно даст вам лучшее представление о том, когда и как использовать C для расширения вашего модуля Python.
Понимание fputs()
fputs()
— это функция библиотеки C, которую вы будете оборачивать:
int fputs(const char *, FILE *)
Эта функция принимает два аргумента:
const char *
— это массив символов.FILE *
— указатель на файловый поток.
fputs()
записывает массив символов в файл, указанный потоком файлов, и возвращает неотрицательное значение. Если операция прошла успешно, то это значение будет обозначать количество байтов, записанных в файл. Если произошла ошибка, возвращается EOF
. Вы можете прочитать больше об этой функции библиотеки C и ее других вариантах в разделе руководства.
Написание функции 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.
Может показаться немного странным увидеть полный код перед объяснением того, как он работает. Однако если вы потратите немного времени на проверку конечного продукта, вы поймете это в следующих разделах. Блок кода ниже показывает окончательную упакованную версию вашего кода C:
#include <Python.h> static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &filename)) return NULL; FILE *fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); }
Этот фрагмент кода ссылается на три объектные структуры, которые определены в Python.h
:
PyObject
PyArg_ParseTuple()
PyLong_FromLong()
Они используются для определения типа данных для языка Python.
PyObject
PyObject
— это структура объектов, которую вы используете для определения типов объектов для Python. Все объекты Python имеют небольшое количество полей, определенных с помощью структуры PyObject
. Все остальные типы объектов являются расширениями этого типа.
PyObject
говорит интерпретатору Python обрабатывать указатель на объект как объект. Например, установка типа возврата вышеуказанной функции как PyObject
определяет общие поля, которые требуются интерпретатору Python для распознавания его как допустимого типа Python.
Взгляните еще раз на первые несколько строк вашего C-кода:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Snip */
В строке 2 вы объявляете типы аргументов, которые хотите получить из кода Python:
char * str
— это строка, которую вы хотите записать в файловый поток.char * filename
— это имя файла для записи.
PyArg_ParseTuple()
PyArg_ParseTuple()
анализирует аргументы, которые вы получите от вашей программы Python, в локальные переменные:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &filename)) return NULL; /* Snip */
Если вы посмотрите на строку 6, то увидите, что PyArg_ParseTuple()
принимает следующие аргументы:
-
args
имеют типPyObject
. -
"ss"
— это спецификатор формата, который определяет тип данных аргументов для анализа. (Вы можете проверить официальную документацию для полной справки.) -
&str
и&filename
являются указателями на локальные переменные, которым будут назначены проанализированные значения.
PyArg_ParseTuple()
оценивается как ложное в случае ошибки. Если произойдет сбой, функция вернет NULL
и больше не будет работать.
fputs()
Как вы видели ранее, fputs()
принимает два аргумента, один из которых — объект FILE *
. Поскольку вы не можете анализировать объект Python textIOwrapper
с помощью Python API в C, вам придется использовать обходной путь:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &filename)) return NULL; FILE *fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); }
Вот разбор того, что делает этот код:
- В строке 10 вы передаете имя файла, который будете использовать для создания объекта
FILE *
и передачи его функции. - В строке 11 вы вызываете
fputs()
со следующими аргументами:
—str
— строка, которую вы хотите записать в файл.
—fp
— это объектFILE *
, который вы определили в строке 10.
Затем вы сохраняете возвращаемое значение fputs()
в bytes_copied
. Эта целочисленная переменная будет возвращена вызову fputs()
в интерпретаторе Python.
PyLong_FromLong (bytes_copied)
PyLong_FromLong()
возвращает PyLongObject
, который представляет целочисленный объект в Python. Вы можете найти его в самом конце вашего C-кода:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &filename)) return NULL; FILE *fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); }
Строка 14 генерирует PyLongObject
для bytes_copied
, переменная, которая возвращается, когда функция вызывается в Python. Вы должны вернуть PyObject *
из вашего модуля расширения Python C обратно в интерпретатор Python.
Написание функции инициализации
Вы написали код, который составляет основную функциональность вашего модуля расширения Python. Тем не менее, есть еще несколько дополнительных функций, которые необходимы для запуска вашего модуля. Вам нужно будет написать определения вашего модуля и методов, которые он содержит, например:
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"
— это имя, которое напишет пользователь для вызова функции из кода на Python. -
method_fputs
— это имя C функции для вызова. -
METH_VARARGS
— это флаг, который сообщает интерпретатору, что функция будет принимать два аргумента типаPyObject *
:
self
— это объект модуля.args
— это кортеж, содержащий фактические аргументы вашей функции. Как объяснялось ранее, эти аргументы распаковываются с использованиемPyArg_ParseTuple()
.
- Последняя строка представляет собой значение, представляющее метод
docstring
.
PyModuleDef
Так же, как PyMethodDef
содержит информацию о методах в вашем модуле расширения Python, структура 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 и структуры методов, пришло время использовать их. Когда программа 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, давайте сделаем шаг назад, чтобы увидеть, как все это сочетается. На следующей диаграмме показаны компоненты вашего модуля и их взаимодействие с интерпретатором Python:
Когда вы импортируете свой модуль расширения Python, PyInit_fputs()
является первым методом, который будет вызван. Однако перед тем, как ссылка возвращается интерпретатору Python, функция выполняет последующий вызов PyModule_Create()
. Это инициализирует структуры PyModuleDef
и PyMethodDef
, которые содержат метаинформацию о вашем модуле. Имеет смысл подготовить их, так как вы будете использовать их в своей функции инициализации.
Как только это завершится, ссылка на объект модуля наконец возвращается интерпретатору Python. Следующая диаграмма показывает внутренний поток вашего модуля:
Объект модуля, возвращаемый PyModule_Create()
, имеет ссылку на структуру модуля PyModuleDef
, которая, в свою очередь, имеет ссылку на таблицу методов PyMethodDef
. Когда вы вызываете метод, определенный в вашем модуле расширения Python, интерпретатор Python использует объект модуля и все ссылки, которые он несет, для выполнения определенного метода. (Хотя это не совсем то, как интерпретатор Python обрабатывает вещи под капотом, он даст вам представление о том, как это работает.)
Точно так же вы можете получить доступ к различным другим методам и свойствам вашего модуля, таким как docstring
модуля или docstring
метода. Они определены внутри их соответствующих структур.
Теперь у вас есть представление о том, что происходит, когда вы вызываете fputs()
из интерпретатора Python. Интерпретатор использует объект вашего модуля, а также ссылки на модуль и метод для вызова метода. Наконец, давайте посмотрим, как интерпретатор обрабатывает фактическое выполнение вашего модуля расширения Python C:
После вызова метода method_fputs()
программа выполняет следующие шаги:
- Разобрать аргументы, которые вы передали от интерпретатора Python, с помощью
PyArg_ParseTuple()
- Передайте эти аргументы в
fputs()
, библиотечную функцию C, которая формирует суть вашего модуля - Используйте
PyLong_FromLong
, чтобы вернуть значение изfputs()
Чтобы увидеть эти же шаги в коде, еще раз взгляните на method_fputs()
:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &filename)) return NULL; FILE *fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); }
Напомним, что ваш метод проанализирует аргументы, переданные вашему модулю, отправит их в fputs()
и вернет результаты.
Упаковка вашего модуля расширения Python C
Прежде чем вы сможете импортировать новый модуль, вам сначала нужно его собрать. Вы можете сделать это, используя пакет дистрибутивов Python.
Для установки приложения вам понадобится файл с именем 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 в текущем каталоге. Если есть какие-либо ошибки или предупреждения, то ваша программа их сейчас выбросит. Убедитесь, что вы исправили их, прежде чем пытаться импортировать свой модуль.
По умолчанию интерпретатор Python использует clang
для компиляции кода на C. Если вы хотите использовать gcc
или любой другой компилятор C для этой работы, то вам необходимо соответствующим образом установить переменную среды CC, либо внутри сценария установки, либо непосредственно в командной строке. Например, вы можете указать интерпретатору Python использовать gcc
для компиляции и сборки вашего модуля следующим образом:
$ CC=gcc python3 setup.py install
Однако интерпретатор Python автоматически вернется к gcc
, если clang
недоступен.
Запуск вашего модуля
Теперь, когда все готово, пришло время увидеть ваш модуль в действии! Как только он будет успешно собран, запустите интерпретатор, чтобы протестировать запуск модуля:
>>> 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, то можете использовать Python API для этого. Вот некоторые функции, предоставляемые 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 в качестве значения исключения
Вы можете использовать любой из них, чтобы вызвать исключение. Однако, что использовать и когда зависит полностью от ваших требований. В Python API есть все стандартные исключения, предварительно определенные как типы PyObject
.
Возникновение исключений из кода C
Хотя вы не можете вызывать исключения в C, Python API позволит вам вызывать исключения из вашего модуля расширения Python C. Давайте проверим эту функциональность, добавив PyErr_SetString()
в ваш код. Это вызовет исключение, если длина записываемой строки меньше 10 символов:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &fd)) return NULL; if (strlen(str) < 10) { PyErr_SetString(PyExc_ValueError, "String length must be greater than 10"); return NULL; } fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); }
Здесь вы проверяете длину входной строки сразу после разбора аргументов и перед вызовом fputs()
. Если строка, переданная пользователем, короче 10 символов, то ваша программа вызовет ошибку ValueError
с пользовательским сообщением. Выполнение программы прекращается, как только возникает исключение.
Обратите внимание, как method_fputs()
возвращает NULL
после вызова исключения. Это связано с тем, что всякий раз, когда вы вызываете исключение с помощью PyErr_*()
, он автоматически устанавливает внутреннюю запись в таблице исключений и возвращает ее. Вызывающая функция не обязана впоследствии устанавливать запись снова. По этой причине вызывающая функция возвращает значение, которое указывает на ошибку, обычно NULL
или -1
. (Это также должно объяснить, почему возникла необходимость возвращать NULL
при разборе аргументов в method_fputs()
с использованием PyArg_ParseTuple()
.)
Возбуждение пользовательских исключений
Вы также можете вызывать пользовательские исключения в вашем модуле расширения Python. Однако все немного по-другому. Ранее в 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; }
Как и прежде, вы начинаете с создания объекта модуля. Затем вы создаете новый объект исключения, используя PyErr_NewException
. Это принимает строку вида module.classname
как имя класса исключения, который вы хотите создать. Выберите что-то описательное, чтобы пользователю было легче интерпретировать то, что на самом деле пошло не так.
Затем вы добавляете это к своему объекту модуля, используя PyModule_AddObject
. Он принимает объект вашего модуля, имя добавляемого нового объекта и сам объект пользовательского исключения в качестве аргументов. Наконец, вы возвращаете объект вашего модуля.
Теперь, когда вы определили настраиваемое исключение для вашего модуля, вам нужно обновить method_fputs()
, чтобы оно вызывало соответствующее исключение:
static PyObject *method_fputs(PyObject *self, PyObject *args) { char *str, *filename = NULL; int bytes_copied = -1; /* Parse arguments */ if(!PyArg_ParseTuple(args, "ss", &str, &fd)) return NULL; if (strlen(str) < 10) { /* Passing custom exception */ PyErr_SetString(StringTooShortError, "String length must be greater than 10"); return NULL; } fp = fopen(filename, "w"); bytes_copied = fputs(str, fp); fclose(fp); return PyLong_FromLong(bytes_copied); }
После создания модуля с новыми изменениями вы можете проверить, что ваше пользовательское исключение работает должным образом, попытавшись написать строку длиной менее 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. Это очень похоже на то, как вы определили пользовательские исключения в предыдущем разделе. Вы можете определить новую константу и добавить ее в свой экземпляр модуля, используя 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; }
Эта функция Python API принимает следующие аргументы:
- Экземпляр вашего модуля.
- Наименование константы.
- Значение константы.
Вы можете сделать то же самое для макросов, используя 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 так же, как и любой другой модуль 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.
Литература для дополнительного чтения:
- Менеджер памяти
- Pandas
- Реализация словаря
- Реализация целого типа в CPython
- Реализация строкового типа в CPython
// Python.h содержит все необходимые функции, для работы с объектами Python
// Эту функцию мы вызываем из Python кода
static PyObject* addList_add(PyObject* self, PyObject* args){
// Входящие аргументы находятся в кортеже
// В нашем случае есть только один аргумент — список, на который мы будем
if (! PyArg_ParseTuple( args, «O», &listObj))
long length = PyList_Size(listObj);
// Проходимся по всем элементам
for(i = 0; i < length; i++){
// Получаем элемент из списка — он также Python-объект
PyObject* temp = PyList_GetItem(listObj, i);
// Мы знаем, что элемент это целое число — приводим его к типу C long
long elem = PyInt_AsLong(temp);
// Возвращаемое в Python-код значение также Python-объект
// Приводим C long к Python integer
return Py_BuildValue(«i», sum);
// Немного документации для `add`
static char addList_docs[] =
«add( ): add all elements of the listn»;
Эта таблица содержит необходимую информацию о функциях модуля
<имя функции в модуле Python>, <фактическая функция>,
<ожидаемые типы аргументов функции>, <документация функции>
static PyMethodDef addList_funcs[] = {
{«add», (PyCFunction)addList_add, METH_VARARGS, addList_docs},
addList имя модуля и это блок его инициализации.
<желаемое имя модуля>, <таблица информации>, <документация модуля>
PyMODINIT_FUNC initaddList(void){
Py_InitModule3(«addList», addList_funcs,
There are several ways in which you can extend the functionality of Python. One of these is to write your Python module in C or C++. This process can lead to improved performance and better access to C library functions and system calls. In this tutorial, you’ll discover how to use the Python API to write Python C extension modules.
You’ll learn how to:
- Invoke C functions from within Python
- Pass arguments from Python to C and parse them accordingly
- Raise exceptions from C code and create custom Python exceptions in C
- Define global constants in C and make them accessible in Python
- Test, package, and distribute your Python C extension module
Extending Your Python Program
One of the lesser-known yet incredibly powerful features of Python is its ability to call functions and libraries defined in compiled languages such as C or C++. This allows you to extend the capabilities of your program beyond what Python’s built-in features have to offer.
There are many languages you could choose from to extend the functionality of Python. So, why should you use C? Here are a few reasons why you might decide to build a Python C extension module:
-
To implement new built-in object types: It’s possible to write a Python class in C, and then instantiate and extend that class from Python itself. There can be many reasons for doing this, but more often than not, performance is primarily what drives developers to turn to C. Such a situation is rare, but it’s good to know the extent to which Python can be extended.
-
To call C library functions and system calls: Many programming languages provide interfaces to the most commonly used system calls. Still, there may be other lesser-used system calls that are only accessible through C. The
os
module in Python is one example.
This is not an exhaustive list, but it gives you the gist of what can be done when extending Python using C or any other language.
To write Python modules in C, you’ll need to use the Python API, which defines the various functions, macros, and variables that allow the Python interpreter to call your C code. All of these tools and more are collectively bundled in the Python.h
header file.
Writing a Python Interface in C
In this tutorial, you’ll write a small wrapper for a C library function, which you’ll then invoke from within Python. Implementing a wrapper yourself will give you a better idea about when and how to use C to extend your Python module.
Understanding fputs()
fputs()
is the C library function that you’ll be wrapping:
int fputs(const char *, FILE *)
This function takes two arguments:
const char *
is an array of characters.FILE *
is a file stream pointer.
fputs()
writes the character array to the file specified by the file stream and returns a non-negative value. If the operation is successful, then this value will denote the number of bytes written to the file. If there’s an error, then it returns EOF
. You can read more about this C library function and its other variants in the manual page entry.
Writing the C Function for fputs()
This is a basic C program that uses fputs()
to write a string to a file stream:
#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;
}
This snippet of code can be summarized as follows:
- Open the file
write.txt
. - Write the string
"Real Python!"
to the file.
In the following section, you’ll write a wrapper for this C function.
Wrapping fputs()
It might seem a little weird to see the full code before an explanation of how it works. However, taking a moment to inspect the final product will supplement your understanding in the following sections. The code block below shows the final wrapped version of your C code:
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}
This code snippet references three object structures which are defined in Python.h
:
PyObject
PyArg_ParseTuple()
PyLong_FromLong()
These are used for data type definition for the Python language. You’ll go through each of them now.
PyObject
PyObject
is an object structure that you use to define object types for Python. All Python objects share a small number of fields that are defined using the PyObject
structure. All other object types are extensions of this type.
PyObject
tells the Python interpreter to treat a pointer to an object as an object. For instance, setting the return type of the above function as PyObject
defines the common fields that are required by the Python interpreter in order to recognize this as a valid Python type.
Take another look at the first few lines of your C code:
1static PyObject *method_fputs(PyObject *self, PyObject *args) {
2 char *str, *filename = NULL;
3 int bytes_copied = -1;
4
5 /* Snip */
In line 2, you declare the argument types you wish to receive from your Python code:
char *str
is the string you want to write to the file stream.char *filename
is the name of the file to write to.
PyArg_ParseTuple()
PyArg_ParseTuple()
parses the arguments you’ll receive from your Python program into local variables:
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 */
If you look at line 6, then you’ll see that PyArg_ParseTuple()
takes the following arguments:
-
args
are of typePyObject
. -
"ss"
is the format specifier that specifies the data type of the arguments to parse. (You can check out the official documentation for a complete reference.) -
&str
and&filename
are pointers to local variables to which the parsed values will be assigned.
PyArg_ParseTuple()
evaluates to false
on failure. If it fails, then the function will return NULL
and not proceed any further.
fputs()
As you’ve seen before, fputs()
takes two arguments, one of which is the FILE *
object. Since you can’t parse a Python textIOwrapper
object using the Python API in C, you’ll have to use a workaround:
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}
Here’s a breakdown of what this code does:
- In line 10, you’re passing the name of the file that you’ll use to create a
FILE *
object and pass it on to the function. - In line 11, you call
fputs()
with the following arguments:str
is the string you want to write to the file.fp
is theFILE *
object you defined in line 10.
You then store the return value of fputs()
in bytes_copied
. This integer variable will be returned to the fputs()
invocation within the Python interpreter.
PyLong_FromLong(bytes_copied)
PyLong_FromLong()
returns a PyLongObject
, which represents an integer object in Python. You can find it at the very end of your C code:
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}
Line 14 generates a PyLongObject
for bytes_copied
, the variable to be returned when the function is invoked in Python. You must return a PyObject*
from your Python C extension module back to the Python interpreter.
Writing the Init Function
You’ve written the code that makes up the core functionality of your Python C extension module. However, there are still a few extra functions that are necessary to get your module up and running. You’ll need to write definitions of your module and the methods it contains, like so:
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
};
These functions include meta information about your module that will be used by the Python interpreter. Let’s go through each of the structs above to see how they work.
PyMethodDef
In order to call the methods defined in your module, you’ll need to tell the Python interpreter about them first. To do this, you can use PyMethodDef
. This is a structure with 4 members representing a single method in your module.
Ideally, there will be more than one method in your Python C extension module that you want to be callable from the Python interpreter. This is why you need to define an array of PyMethodDef
structs:
static PyMethodDef FputsMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
{NULL, NULL, 0, NULL}
};
Each individual member of the struct holds the following info:
-
"fputs"
is the name the user would write to invoke this particular function. -
method_fputs
is the name of the C function to invoke. -
METH_VARARGS
is a flag that tells the interpreter that the function will accept two arguments of typePyObject*
:self
is the module object.args
is a tuple containing the actual arguments to your function. As explained previously, these arguments are unpacked usingPyArg_ParseTuple()
.
- The final string is a value to represent the method docstring.
PyModuleDef
Just as PyMethodDef
holds information about the methods in your Python C extension module, the PyModuleDef
struct holds information about your module itself. It is not an array of structures, but rather a single structure that’s used for module definition:
static struct PyModuleDef fputsmodule = {
PyModuleDef_HEAD_INIT,
"fputs",
"Python interface for the fputs C library function",
-1,
FputsMethods
};
There are a total of 9 members in this struct, but not all of them are required. In the code block above, you initialize the following five:
-
PyModuleDef_HEAD_INIT
is a member of typePyModuleDef_Base
, which is advised to have just this one value. -
"fputs"
is the name of your Python C extension module. -
The string is the value that represents your module docstring. You can use
NULL
to have no docstring, or you can specify a docstring by passing aconst char *
as shown in the snippet above. It is of typePy_ssize_t
. You can also usePyDoc_STRVAR()
to define a docstring for your module. -
-1
is the amount of memory needed to store your program state. It’s helpful when your module is used in multiple sub-interpreters, and it can have the following values:- A negative value indicates that this module doesn’t have support for sub-interpreters.
- A non-negative value enables the re-initialization of your module. It also specifies the memory requirement of your module to be allocated on each sub-interpreter session.
-
FputsMethods
is the reference to your method table. This is the array ofPyMethodDef
structs you defined earlier.
For more information, check out the official Python documentation on PyModuleDef
.
PyMODINIT_FUNC
Now that you’ve defined your Python C extension module and method structures, it’s time to put them to use. When a Python program imports your module for the first time, it will call PyInit_fputs()
:
PyMODINIT_FUNC PyInit_fputs(void) {
return PyModule_Create(&fputsmodule);
}
PyMODINIT_FUNC
does 3 things implicitly when stated as the function return type:
- It implicitly sets the return type of the function as
PyObject*
. - It declares any special linkages.
- It declares the function as extern “C.” In case you’re using C++, it tells the C++ compiler not to do name-mangling on the symbols.
PyModule_Create()
will return a new module object of type PyObject *
. For the argument, you’ll pass the address of the method structure that you’ve already defined previously, fputsmodule
.
Putting It All Together
Now that you’ve written the necessary parts of your Python C extension module, let’s take a step back to see how it all fits together. The following diagram shows the components of your module and how they interact with the Python interpreter:
When you import your Python C extension module, PyInit_fputs()
is the first method to be invoked. However, before a reference is returned to the Python interpreter, the function makes a subsequent call to PyModule_Create()
. This will initialize the structures PyModuleDef
and PyMethodDef
, which hold meta information about your module. It makes sense to have them ready since you’ll make use of them in your init function.
Once this is complete, a reference to the module object is finally returned to the Python interpreter. The following diagram shows the internal flow of your module:
The module object returned by PyModule_Create()
has a reference to the module structure PyModuleDef
, which in turn has a reference to the method table PyMethodDef
. When you call a method defined in your Python C extension module, the Python interpreter uses the module object and all of the references it carries to execute the specific method. (While this isn’t exactly how the Python interpreter handles things under the hood, it’ll give you an idea of how it works.)
Similarly, you can access various other methods and properties of your module, such as the module docstring or the method docstring. These are defined inside their respective structures.
Now you have an idea of what happens when you call fputs()
from the Python interpreter. The interpreter uses your module object as well as the module and method references to invoke the method. Finally, let’s take a look at how the interpreter handles the actual execution of your Python C extension module:
Once method_fputs()
is invoked, the program executes the following steps:
- Parse the arguments you passed from the Python interpreter with
PyArg_ParseTuple()
- Pass these arguments to
fputs()
, the C library function that forms the crux of your module - Use
PyLong_FromLong
to return the value fromfputs()
To see these same steps in code, take a look at method_fputs()
again:
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}
To recap, your method will parse the arguments passed to your module, send them on to fputs()
, and return the results.
Packaging Your Python C Extension Module
Before you can import your new module, you first need to build it. You can do this by using the Python package distutils
.
You’ll need a file called setup.py
to install your application. For this tutorial, you’ll be focusing on the part specific to the Python C extension module. For a full primer, check out How to Publish an Open-Source Python Package to PyPI.
A minimal setup.py
file for your module should look like this:
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()
The code block above shows the standard arguments that are passed to setup()
. Take a closer look at the last positional argument, ext_modules
. This takes a list of objects of the Extensions
class. An object of the Extensions
class describes a single C or C++ extension module in a setup script. Here, you pass two keyword arguments to its constructor, namely:
name
is the name of the module.[filename]
is a list of paths to files with the source code, relative to the setup script.
Building Your Module
Now that you have your setup.py
file, you can use it to build your Python C extension module. It’s strongly advised that you use a virtual environment to avoid conflicts with your Python environment.
Navigate to the directory containing setup.py
and run the following command:
$ python3 setup.py install
This command will compile and install your Python C extension module in the current directory. If there are any errors or warnings, then your program will throw them now. Make sure you fix these before you try to import your module.
By default, the Python interpreter uses clang
for compiling the C code. If you want to use gcc
or any other C compiler for the job, then you need to set the CC
environment variable accordingly, either inside the setup script or directly on the command line. For instance, you can tell the Python interpreter to use gcc
to compile and build your module this way:
$ CC=gcc python3 setup.py install
However, the Python interpreter will automatically fall back to gcc
if clang
is not available.
Running Your Module
Now that everything is in place, it’s time to see your module in action! Once it’s successfully built, fire up the interpreter to test run your Python C extension module:
>>>
>>> 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!'
Your function performs as expected! You pass a string "Real Python!"
and a file to write this string to, write.txt
. The call to fputs()
returns the number of bytes written to the file. You can verify this by printing the contents of the file.
Also recall how you passed certain arguments to the PyModuleDef
and PyMethodDef
structures. You can see from this output that Python has used these structures to assign things like the function name and docstring.
With that, you have a basic version of your module ready, but there’s a lot more that you can do! You can improve your module by adding things like custom exceptions and constants.
Raising Exceptions
Python exceptions are very different from C++ exceptions. If you want to raise Python exceptions from your C extension module, then you can use the Python API to do so. Some of the functions provided by the Python API for exception raising are as follows:
Function | Description |
---|---|
PyErr_SetString(PyObject *type, const char *message) |
Takes two arguments: a PyObject * type argument specifying the type of exception, and a custom message to display to the user |
PyErr_Format(PyObject *type, const char *format) |
Takes two arguments: a PyObject * type argument specifying the type of exception, and a formatted custom message to display to the user |
PyErr_SetObject(PyObject *type, PyObject *value) |
Takes two arguments, both of type PyObject * : the first specifies the type of exception, and the second sets an arbitrary Python object as the exception value |
You can use any of these to raise an exception. However, which to use and when depends entirely on your requirements. The Python API has all the standard exceptions pre-defined as PyObject
types.
Raising Exceptions From C Code
While you can’t raise exceptions in C, the Python API will allow you to raise exceptions from your Python C extension module. Let’s test this functionality by adding PyErr_SetString()
to your code. This will raise an exception whenever the length of the string to be written is less than 10 characters:
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}
Here, you check the length of the input string immediately after you parse the arguments and before you call fputs()
. If the string passed by the user is shorter than 10 characters, then your program will raise a ValueError
with a custom message. The program execution stops as soon as the exception occurs.
Note how method_fputs()
returns NULL
after raising the exception. This is because whenever you raise an exception using PyErr_*()
, it automatically sets an internal entry in the exception table and returns it. The calling function is not required to subsequently set the entry again. For this reason, the calling function returns a value that indicates failure, usually NULL
or -1
. (This should also explain why there was a need to return NULL
when you parse arguments in method_fputs()
using PyArg_ParseTuple()
.)
Raising Custom Exceptions
You can also raise custom exceptions in your Python C extension module. However, things are a bit different. Previously, in PyMODINIT_FUNC
, you were simply returning the instance returned by PyModule_Create
and calling it a day. But for your custom exception to be accessible by the user of your module, you need to add your custom exception to your module instance before you return it:
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;
}
As before, you start off by creating a module object. Then you create a new exception object using PyErr_NewException
. This takes a string of the form module.classname
as the name of the exception class that you wish to create. Choose something descriptive to make it easier for the user to interpret what has actually gone wrong.
Next, you add this to your module object using PyModule_AddObject
. This takes your module object, the name of the new object being added, and the custom exception object itself as arguments. Finally, you return your module object.
Now that you’ve defined a custom exception for your module to raise, you need to update method_fputs()
so that it raises the appropriate exception:
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}
After building the module with the new changes, you can test that your custom exception is working as expected by trying to write a string that is less than 10 characters in length:
>>>
>>> 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
When you try to write a string with fewer than 10 characters, your custom exception is raised with a message explaining what went wrong.
Defining Constants
There are cases where you’ll want to use or define constants in your Python C extension module. This is quite similar to how you defined custom exceptions in the previous section. You can define a new constant and add it to your module instance using 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;
}
This Python API function takes the following arguments:
- The instance of your module
- The name of the constant
- The value of the constant
You can do the same for macros using 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;
}
This function takes the following arguments:
- The instance of your module
- The name of the macro that has already been defined
Open up the Python interpreter to see if your constants and macros are working as expected:
>>>
>>> import fputs
>>> # Constants
>>> fputs.FPUTS_FLAG
64
>>> fputs.FPUTS_MACRO
256
Here, you can see that the constants are accessible from within the Python interpreter.
Testing Your Module
You can test your Python C extension module just as you would any other Python module. This can be demonstrated by writing a small test function for 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
In the test script above, you use fputs.fputs()
to write the string "Real Python!"
to an empty file named test_write.txt
. Then, you read in the contents of this file and use an assert
statement to compare it to what you had originally written.
You can run this test suite to make sure your module is working as expected:
$ pytest -q
test_fputs.py [100%]
1 passed in 0.03 seconds
For a more in-depth introduction, check out Getting Started With Testing in Python.
Considering Alternatives
In this tutorial, you’ve built an interface for a C library function to understand how to write Python C extension modules. However, there are times when all you need to do is invoke some system calls or a few C library functions, and you want to avoid the overhead of writing two different languages. In these cases, you can use Python libraries such as ctypes
or cffi
.
These are Foreign Function libraries for Python that provide access to C library functions and data types. Though the community itself is divided as to which library is best, both have their benefits and drawbacks. In other words, either would make a good choice for any given project, but there are a few things to keep in mind when you need to decide between the two:
-
The
ctypes
library comes included in the Python standard library. This is very important if you want to avoid external dependencies. It allows you to write wrappers for other languages in Python. -
The
cffi
library is not yet included in the standard library. This might be a dealbreaker for your particular project. In general, it’s more Pythonic in nature, but it doesn’t handle preprocessing for you.
For more information on these libraries, check out Extending Python With C Libraries and the “ctypes” Module and Interfacing Python and C: The CFFI Module.
Conclusion
In this tutorial, you’ve learned how to write a Python interface in the C programming language using the Python API. You wrote a Python wrapper for the fputs()
C library function. You also added custom exceptions and constants to your module before building and testing it.
The Python API provides a host of features for writing complex Python interfaces in the C programming language. At the same time, libraries such as cffi
or ctypes
can lower the amount of overhead involved in writing Python C extension modules. Make sure you weigh all the factors before making a decision!
Простой, а главное — рабочий пример модуля расширения на языке C для питона. Модуль называется example, а реализовывает одну функцию hello, которая вызывается с параметром who и возвращает «Hello %s» % who.
Листинг модуля example.c
#include <Python.h> PyObject *hello( PyObject *self, PyObject *args, PyObject *kwargs ) { char *who = 0; static char *keywords[] = {"who", NULL}; PyObject *result = 0; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, &who)) { return NULL; } result = PyString_FromString("Hello "); PyString_Concat(&result, PyString_FromString(who)); return result; } static PyMethodDef example_methods[] = { { "hello", (PyCFunction) hello, METH_KEYWORDS, "hello(who) -- return "Hello who"" }, { NULL, 0, 0, NULL } }; PyMODINIT_FUNC initexample() { (void) Py_InitModule("example", example_methods); if (PyErr_Occurred()) { PyErr_SetString(PyExc_ImportError, "example module init failed"); } }
Листинг setup.py
from distutils.core import setup from distutils.extension import Extension examplemodule = Extension(name="example", sources=['example.c', ]) setup(name="example", ext_modules=[examplemodule])
Компилируем:
Инсталлируем:
Выполняем простенький тест:
from example import hello print hello(who="world!")
Радуемся
Полезные ссылки (на английском):
- Как создать пакет для распространения своего модуля
- Что делать клиенту после получения этого пакета
Цикл статей про Boost.Python на хабре:
- Объединяя C++ и Python. Тонкости Boost.Python. Часть первая
- Объединяя C++ и Python. Тонкости Boost.Python. Часть вторая
- Конвертация типов в Boost.Python. Делаем преобразование между привычными типами C++ и Python
- Путешествие исключений между C++ и Python или «Туда и обратно»
UPD: Для компиляции под Python 3.x вам нужен другой example.c:
#include <Python.h> PyObject *hello( PyObject *self, PyObject *args, PyObject *kwargs ) { char *who = 0; static char *keywords[] = {"who", NULL}; if (PyArg_ParseTupleAndKeywords(args, kwargs, "s", keywords, &who)) { return PyUnicode_FromFormat("Hello %s", who); } return NULL; } static PyMethodDef example_methods[] = { { "hello", (PyCFunction) hello, METH_KEYWORDS, "hello(who) -- return "Hello who"" }, { NULL, 0, 0, NULL } }; static struct PyModuleDef example_module = { PyModuleDef_HEAD_INIT, "example", NULL, -1, example_methods }; PyMODINIT_FUNC PyInit_example(void) { return PyModule_Create(&example_module); }