Жажда скорости: Python с расширениями С

Благодаря сочетанию простоты и эффективности Python стал одним из самых популярных языков программирования. Но при этом ему иногда не хватает столь высоко ценимой скорости статически типизированных и предкомпилируемых языков, как С и Java. 

Почему же Python медленный? 

Как известно, типичной реализацией Python является интерпретатор. Этим и объясняется более медленное выполнение кода по сравнению с языками С и Java, для которых характерны компилируемые реализации и исходный код, предварительно скомпилированный в машинный. Однако данная тема выходит за рамки этой статьи. 

Как можно ускорить код Python? 

Если вам не нужно выполнять сложные вычислительные операции, медлительность Python, как правило, проблемы не составляет. В других же случаях выручают расширения С. 

Они позволяют писать функции в С, затем компилировать их в модуль Python и использовать в исходном коде как его обычную библиотеку. 

В действительности большинство общеизвестных модулей написаны на C или C++ (например, numpy, pandas, tensorflow) для лучшей производительности и/или низкоуровневых функциональностей. 

Обратите внимание, что: 

  • Расширения С подходят только для реализации Cpython. А поскольку он является интерпретатором по умолчанию, то каких-либо проблем не предвидится. 
  • Желательно наличие базовых знаний языка программирования С. Однако, имея в арсенале только Python, вы также сможете беспрепятственно следовать данному руководству. 

Как создать расширение С? 

В качестве примера реализуем классическую функцию fib(n). Она принимает число n и возвращает соответствующее число в последовательности Фибоначчи. После этого сравним производительность версий Python и С. 

Прежде всего, потребуется API Python/C Python.h. Это заголовочный файл С, содержащий все необходимое для взаимодействия с Python.

Установка Python API

  • Как правило, в Linux требуется установить пакет python-dev или python3-dev при условии его отсутствия. (Обратите внимание, что в некоторых дистрибутивах имя пакета может отличаться). 
  • По умолчанию, если Windows устанавливается через стандартный установщик, то Python должен идти комплектом. 
  • MacOs также поставляется с Python. Если же нет, то с помощью brew reinstall python можно исправить ситуацию. 

Теперь открываем предпочтительный редактор кода и создаем файл модуля С. Его имя должно соответствовать соглашению  —  нечто вроде module_name.c, хотя возможны и любые другие варианты. В данном случае назовем его c_module.c.

Перед написанием кода расширения необходимо включить ряд основных определений и объявлений. 

// Данное определение необходимо для сохранения актуальности кода в будущих случаях использования 
// Перейдите по ссылке https://docs.python.org/3/c-api/arg.html#:~:text=Note%20For%20all,always%20define%20PY_SSIZE_T_CLEAN.
#define PY_SSIZE_T_CLEAN
// Фактический Python API
#include <Python.h>

В целях совместимости рекомендуется размещать эти строки в начале файла. 

Поскольку в Python все является объектом, функция c_fib(n) должна вернуть таковой, а именно указатель PyObject (определенный в Python.h).

// Чистая функция С, которая будет вызываться рекурсивно 
int fib(int n)
{
if (n <= 1)
return n;
return fib(n-1) + fib(n-2);
}

// Функция, которая будет вызываться из кода Python,
// оборачивается вокруг чистой функции С fib
PyObject* c_fib(PyObject* self, PyObject* args)
{
int n;
PyArg_ParseTuple(args, "i", &n);
n = fib(n);
return PyLong_FromLong(n);
}

Далее необходимо объявить функции, экспортируемые из модуля, обеспечив Python доступ к ним. 

// Массив, содержащий определения методов модуля 
// Здесь размещаем методы для экспорта
// Массив должен заканчиваться структурой {NULL}
PyMethodDef module_methods[] =
{
{"c_fib", c_fib, METH_VARARGS, "Method description"},
{NULL} // Эта структура говорит об окончании массива
};

// Структура, представляющая модуль
struct PyModuleDef c_module =
{
PyModuleDef_HEAD_INIT, // Всегда инициализируем этот компонент как PyModuleDef_HEAD_INIT
"c_module", // имя модуля
"Module description", // описание модуля
-1, // размер модуля (об этом далее)
module_methods // методы, связанные с модулем
};

// Функция, которая инициализирует модуль
PyMODINIT_FUNC PyInit_c_module()
{
return PyModule_Create(&c_module);
}

Определение методов модуля 

Каждый экспортируемый метод представлен в виде структуры, состоящей из: 

  • имени экспортируемого метода (в данном случае "c_fib”); 
  • фактического метода для экспорта (c_fib); 
  • типа принимаемых методом аргументов (в нашем случае METH_VARARGS). Приведем отрывок из документации по METH_VARARGS: “Это стандартное соглашение о вызове, согласно которому методы имеют тип PyCFunction. Функция ожидает два значения PyObject*. Первое из них  —  объект модуля self для методов и функций модуля. Второй параметр (часто называемый args)  —  кортеж, представляющий все аргументы”. 
  • const char* для описания метода. 

Определение модуля 

Модуль представлен в виде структуры, как показано в приведенном выше коде. Он самодокументирующийся, за исключением аргумента m_size, которому было установлено значение -1. Обратимся к документации: 

Значение -1 у аргумента m_size означает, что модуль не поддерживает субинтерпретаторы, поскольку обладает глобальным состоянием. 

Функция инициализации модуля 

Функция PyMODINIT_FUNC вызывается при импорте модуля и инициализирует его. Обратите внимание, что имя функции должно начинаться с PyInit_ и заканчиваться именем модуля, отсюда PyInit_c_module().

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

Компиляция расширения в модуль Python 

По мере готовности кода С вы должны скомпилировать его в модуль Python. К счастью, для этой цели у нас есть немало встроенных инструментов. 

Создаем скрипт Python, по традиции именуемый setup.py, и вставляем следующий код: 

# Импорт инструментов для создания расширения С 
from distutils.core import setup, Extension

module_name = 'c_module'
# Файлы, из которых состоит расширение
c_files = ['c_module.c']

extension = Extension(
module_name,
c_files
)

setup(
name=module_name,
version='1.0',
description='The package description',
author='Nicholas Obert',
author_email='[email protected]',
url='https://my.web.site/some_page',
ext_modules=[extension]
)

У данного скрипта много функциональностей, но нам потребуются только команды build и install. За более подробной информацией можно обратиться к документации или выполнить скрипт с флагом help:

python3 setup.py --help

В терминале выполняем команду: 

python3 setup.py build

Она создает директорию build и помещает в нее скомпилированные библиотеки. Далее выполняем команду: 

python3 setup.py install

Она устанавливает созданные библиотеки в систему, обеспечивая к ним доступ из любого места. 

Обратите внимание, что для этого вам могут потребоваться права root/admin. Выполнять установку в систему не обязательно, но если этот процесс пропустить, то для работы с расширением придется использовать относительные импорты

Применение расширения С в программе Python 

В файл Python импортируем вновь созданный модуль с именем по выбору. В нашем примере это c_module

import c_module
print(c_module.c_fib(5))
# вывод: 5

Как видим, расширение используется точно так же, как и любой другой модуль Python. 

Сравнение с обычной версией Python 

Сравним функцию c_fib с ее обычным аналогом Python. Воспользуемся встроенным модулем time:

import c_module
from time import time

# Версия fib Python с рекурсией
def py_fib(n):
if (n <= 1):
return n
return py_fib(n-1) + py_fib(n-2)

n = 5

# тестируем C
t = time()
c_res = c_module.c_fib(n)
c_time = time() - t

# тестируем Python
t = time()
py_res = py_fib(n)
py_time = time() - t

print(f'Input: {n}\n{py_res=}, {py_time=}\n{c_res=}, {c_time=}')

Вывод:

Input: 5
py_res=5, py_time=5.245208740234375e-06
c_res=5, c_time=1.6689300537109375e-06

Как и ожидалось, функция С быстрее. 

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

Приведем пример с большими числами: 

Input: 10
py_res=55, py_time=3.147125244140625e-05
c_res=55, c_time=2.6226043701171875e-06Input: 30
py_res=832040, py_time=0.40490126609802246
c_res=832040, c_time=0.004115581512451172Input: 40
py_res=102334155, py_time=50.17047834396362
c_res=102334155, c_time=0.4414968490600586

Когда дело касается больших чисел, то версия С очевидно превосходит аналог Python. При необходимости выполнения нескольких простых вычислений, возможно, нет смысла реализовывать их в С, поскольку разница в производительности будет минимальной. Однако в случае действительно времязатратной операции или функции, требующей многократных повторений, скорости Python может оказаться недостаточно. 

Именно в таких ситуациях расширения С раскрывают всю свою мощь. Вы можете переложить всю тяжелую работу на С, а Python использовать в качестве основного языка. 

Расширения С в практической деятельности 

Допустим, необходимо выполнить ряд сложных вычислений, будь то криптоалгоритм, машинное обучение или обработка больших объемов данных. Расширения С могут взять долю нагрузки Python на себя и ускорить работу приложения. 

Решили создать низкоуровневый интерфейс или поработать напрямую с памятью из Python? Расширения С к вашим услугам, учитывая, что вы знаете, как задействовать необработанные указатели. 

Задумали улучшить уже существующее, но плохо работающее приложение Python и при этом не хотите (или не можете) переписать его на другом языке? Выход есть  —  расширения С. 

А может вы просто убежденный сторонник оптимизации, который стремится максимально ускорить выполнение своего кода, при этом не отказываясь от высокоуровневых абстракций для работы в сети, GUI и т.д. 

Мы всегда испытываем дефицит времени, поэтому инвестировать его следует с умом. 

Заключение 

Будь вы разработчиком Python, замороченным на производительности и эффективности, или сторонником комбинирования различных технологий, или просто экспериментатором с жаждой ко всему новенькому, расширения С для Python станут отличным пополнением вашего рабочего инструментария. Они не только гарантируют вам производительность практически задаром, но и расширяют возможности Python, избавляя его от стека устаревших технологий. 

Благодарю за внимание! 

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Nicholas Obert: Speed Up Your Python Codebases With C Extensions

Предыдущая статьяО нейронных сетях в двух словах
Следующая статьяСоставные типы данных на Golang