Благодаря сочетанию простоты и эффективности 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, избавляя его от стека устаревших технологий.
Благодарю за внимание!
Читайте также:
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Nicholas Obert: Speed Up Your Python Codebases With C Extensions