…и стоит ли нам это делать?

Прежде чем посмотреть, «как» использовать код C из Python, давайте сначала посмотрим, «почему» кто-то может захотеть это сделать. Если вы читаете эту статью прямо сейчас, вы, вероятно, хотите сделать одну или несколько из этих трех вещей:

  1. Повторное использование существующего кода C из программы Python
  2. Ускорьте свою программу Python
  3. Делайте некоторые низкоуровневые вещи, которые нельзя сделать напрямую в Python.

Ярлык…

Если все, что вам нужно, это просто ускорить вашу программу на Python, то на самом деле есть более простой способ, чем писать определенные части вашей программы на C. Вы можете просто использовать PyPy вместо Python при выполнении вашего приложения. PyPy — это альтернативная реализация языка программирования Python, в которой используется своевременная компиляция для ускорения того же кода Python с небольшими изменениями или без изменений в вашем коде. Скачать и установить его можно здесь.

После установки просто замените python на pypy при выполнении скрипта Python. Итак, вместо:

python my_awesome_program.py

do:

pypy my_awesome_program.py

Как вы увидите в тесте в конце этой статьи, этот метод может значительно ускорить ваш код, а в некоторых ситуациях (как в случае с примером простой программы, показанным ниже в этой статье) он может быть даже быстрее, чем C выполнение.

Если вы действительно хотите написать/повторно использовать C…

Далее мы рассмотрим 2 способа использования C в Python, а именно:

  1. Использование модуля ctypes для вызова функций C
  2. Написание собственного модуля Python на C

Библиотека ctypes предоставляет типы данных, совместимые с C, и позволяет вызывать функции в библиотеках DLL или разделяемых библиотеках. Его можно использовать для обертывания этих библиотек в чистый Python.

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

С другой стороны, если у вас нет существующего кода на C и вы хотите написать его прямо сейчас специально для использования в Python для ускорения или выполнения каких-то низкоуровневых задач, то написание модуля Python на C может быть лучшим вариантом. При написании модуля Python на C у вас есть доступ почти ко всем встроенным типам данных Python и их методам (в виде простых функций C). Это упрощает написание вашей программы на C специально для Python, а затем, когда вы импортируете модуль в Python, вам не нужно больше работать, чтобы «адаптировать» его к Python. Вы просто используете модуль так, как он был написан на Python.

Использование библиотеки ctypes для вызова функций C

ctypes экспортирует несколько объектов, которые можно использовать для загрузки динамических библиотек, и они используют разные соглашения о вызовах: cdll, windll, oledll. В этой статье мы будем использовать cdll.

Чтобы загрузить библиотеку:

mylib = cdll.LoadLibrary('library path or name')

После загрузки библиотеки мы можем получить доступ к функции внутри нее следующим образом:

my_awesome_func = mylib.my_awesome_func
my_awesome_func() # call the function

Но подождите... Эти функции C, которые мы хотим вызвать, ожидают типы данных C в качестве параметров и возвращают типы данных C. Как мы можем научить Python работать с этими типами? К счастью, библиотека ctypes также экспортирует объекты, представляющие типы данных C, и может использоваться для преобразования переменных Python при передаче их в качестве параметров функциям C и для информирования Python о том, какой тип следует ожидать в качестве возвращаемого значения из функции C.

Вот список с большинством доступных типов:

  • c_byte
  • c_char
  • c_char_p
  • c_double
  • c_longdouble
  • c_float
  • c_int
  • c_int8
  • c_int16
  • c_int32
  • c_int64
  • c_long
  • c_longlong
  • c_short
  • c_size_t
  • c_ssize_t
  • c_ubyte
  • c_uint
  • c_uint8
  • c_uint16
  • c_uint32
  • c_uint64
  • c_ulong
  • c_ulonglong
  • c_ushort
  • c_void_p
  • c_wchar
  • c_wchar_p
  • c_bool

И если вам нужен указатель одного из этих типов, просто используйте функцию POINTER(type) следующим образом:

pointer_to_int_type = POINTER(c_int)

Чтобы сообщить Python о типе возврата функции C, используйте атрибут .restype, например:

my_awesome_func.restype = POINTER(c_int) # my_awesome_func returns a pointer to int

И чтобы передать значения функциям в правильном типе, просто оберните их в соответствующий конструктор ctype:

my_awesome_func(c_int(300)) # call function with 300 as a C int type

Предположим, у нас есть следующий существующий код C, который мы хотели бы использовать в программе Python:

Довольно просто, правда? Это просто функция fib(), которая возвращает массив с первыми n числами Фибоначчи. И мы хотим вызвать эту функцию в Python и показать результаты на экране.

Во-первых, нам нужно скомпилировать этот код в разделяемую библиотеку:

gcc -c -fpic fib.c
gcc -shared -o libfib.so fib.o

Затем, чтобы вызвать его в Python, мы делаем:

Я думаю, что приведенный выше код с комментариями говорит сам за себя, но давайте все же проясним некоторые вещи:

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

Если вы хотите узнать больше о ctypes, вот официальная документация.

Написание собственного модуля Python на C

Так как же написать такой модуль Python? Мы начнем только с файла C (или нескольких файлов, если ваш модуль более сложный и его нужно разделить на несколько файлов; но для простоты предположим, что у нас есть только один файл C). Но этот файл C должен следовать некоторым специальным правилам, чтобы его можно было скомпилировать в модуль Python.

Директивы препроцессора

Нам нужно определить макрос (#define PY_SSIZE_T_CLEAN) и включить заголовочный файл Python (#include <Python.h>). Этот заголовочный файл объявляет (на самом деле не объявляет себя, а включает в себя в свою очередь другие файлы; но вы поняли) все функции и типы данных, которые вам нужны для связи с интерпретатором Python (работа с объектами Python, полученными в качестве параметров, создавать объекты Python и возвращать их и т. д.). И убедитесь, что вы определили PY_SSIZE_T_CLEAN перед включением файла заголовка Python. Итак, в начале файла у нас должно быть что-то вроде:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

Создание реальных функций нашего модуля

Функции, которые мы хотим экспортировать в Python из нашего модуля, должны быть созданы по следующему шаблону:

static PyObject * func_name(PyObject *self, PyObject *args)
{
// Parse arguments with PyArg_ParseTuple(args, ...)
// Do other stuff, maybe create some Python objects like a List, Tuple, Integer object etc.
// Return one of these objects, or maybe nothing (NULL)
}

Здесь «func_name» может быть любым, что мы захотим, но общепринятым соглашением об именах является использование «‹имя модуля›_‹имя функции, как оно появляется в Python›» в качестве имени наших функций.

PyObject — это общий тип данных, который может представлять любой объект, используемый в Python, например: целые числа, числа с плавающей запятой, списки, кортежи и т. д.

Эти функции получают 2 параметра: self и args, оба они являются указателями на PyObject. Self указывает на объект модуля, поэтому мы можем использовать его для хранения внутреннего состояния нашего модуля или доступа к другим вещам внутри нашего модуля. Args указывает на кортеж позиционных аргументов или словарь ключевых слов -> аргументы. Чтобы использовать эти аргументы в нашем коде C, нам нужно проанализировать их с помощью PyArg_ParseTuple() или PyArg_ParseTupleAndKeywords().

Позиционные аргументы анализируются с помощью:

PyArg_ParseTuple(args, "format_string1", &variable1, "format_string2", &variable2, ...)

Где: format_stringN — это строка, указывающая, какой тип данных хранить в переменной N. Этот код сохраняет аргументы, которые передаются при вызове функции в Python, в переменные1, переменная2, …, которые являются переменными C. PyArg_ParseTuple() возвращает true в случае успеха или false в противном случае. Здесь вы можете найти полный список со строками формата.

Затем, чтобы делать другие вещи, которые мы можем захотеть сделать внутри нашей функции и работать с объектами Python, такими как числа, списки, кортежи и т. д., нам нужно использовать функции, определенные в Python.h, которых слишком много, чтобы сказать кое-что о каждом из них здесь, так что вы можете посмотреть в документации; здесь вы можете найти все эти функции.

Создание массива методов

Интерпретатор Python не знает автоматически, как найти в нашем модуле все функции, которые мы создали. Поэтому нам нужно создать сопоставление «имя функции, которое мы хотим отобразить в Python» -> указатель на одну из функций в предыдущем разделе. Затем сохраните это сопоставление в переменной, которую мы экспортируем в Python. Вот как мы это делаем:

static PyMethodDef methods_array[] = {
    {"py_name",  c_name, METH_VARARGS, "description"},
    // ...
    {NULL, NULL, 0, NULL} // Sentinel
};

Где:

  • «py_name» — это имя, которое будет использоваться в Python.
  • c_name — это имя нашей C-функции (указатель на функцию)
  • затем мы используем либо METH_VARARGS– для позиционных аргументов, либо METH_VARARGS | METH_KEYWORDS – если мы хотим использовать ключевые слова
  • «Описание» может быть любой строкой или NULL

Наш methods_array всегда должен иметь {NULL, NULL, 0, NULL} в качестве последнего элемента.

Создание структуры модуля

После того, как мы создали methods_array, нам нужно создать переменную, которая содержит информацию о модуле в целом (methods_array является одним из его членов). Делаем так:

static struct PyModuleDef module_name = {
    PyModuleDef_HEAD_INIT,
    "module_name",
    "module description/documentation",
    -1, /* size of per-interpreter state of the module,
           or -1 if the module keeps state in global variables. */
    methods_array
};

Создайте функцию инициализации

Наконец, мы создаем функцию, которая инициализирует наш модуль, используя переменную module_name из предыдущего шага. Эта функция инициализации должна быть единственной нестатической вещью в файле C. Вот как это выглядит:

PyMODINIT_FUNC PyInit_module_name(void)
{
    return PyModule_Create(&module_name);
}

Пример

В качестве примера здесь мы выполним ту же функцию fib, что и ранее, на простом C, но на этот раз мы сделаем это как модуль Python.

Вот полный код этого модуля Python:

Соберите модуль

Для сборки модуля мы используем distutils библиотеку Python и создаем setup.py скрипт следующим образом:

from distutils.core import setup, Extension

module1 = Extension(name='module_name',
                    sources=['file1.c', 'file2.c', ...],
                    include_dirs=[], # list of directories to search for C/C++ header files
                    library_dirs=[], # list of directories to search for static libraries
                    runtime_library_dirs=[], # list of directories to search for shared libraries
                    libraries=[] # list of library names)

setup (name = 'module_name',
       version = '1.0',
       description = '...',
       ext_modules = [module1])

После завершения скрипта setup.py мы можем запустить:

python setup.py build

построить его.

Если все работает нормально и ошибок компиляции нет, у нас должна быть папка сборки в том же родительском каталоге, что и файл setup.py. Внутри сборки вы найдете папку «lib.‹your os name›», а внутри нее находится скомпилированный модуль Python.

Or:

python setup.py install

чтобы установить его в нужном месте.

После того, как он будет установлен, мы сможем импортировать его, используя имя модуля, которое мы установили ранее. Мы также можем использовать его без установки; сразу после его сборки, если мы создадим файл Python в той же папке, что и результирующий файл .so (Unix) или .pyd (Windows), он должен работать для импорта из него, используя имя модуля, которое мы определили (а не весь имя скомпилированного файла).

Если вы работаете в Windows, вам потребуется установить MS Visual Studio (и отметить параметр C++ при установке, поскольку требуется компилятор C/C++), чтобы сценарий установки работал.

Если вы хотите узнать больше о процессе сборки, вот документация.

Вернемся к нашему примеру

Вот файл setup.py для нашего «my_c_module» с функцией fib:

# setup.py
from distutils.core import setup, Extension

module1 = Extension('my_c_module',
                    sources = ['fib_c_module.c'])

setup (name = 'my_c_module',
       version = '1.0',
       description = 'This is a demo package',
       ext_modules = [module1])

После сборки или установки мы можем использовать модуль в файле Python:

from my_c_module import fib

print(fib(20))

# Outputs:
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]

Собираем все вместе и делаем несколько тестов

Теперь давайте возьмем все 3 способа написания нашего примера функции fib(): 1) Чистый Python, 2) Модуль Python C, 3) Чистая функция C, вызываемая из Python, и используем их в одном файле Python и сравним его, чтобы увидеть, какой из них Быстрее.

Во-первых, давайте реализуем функцию fib() на Python:

И здесь мы используем их все:

Теперь давайте проверим это с помощью timeit. Мы собираемся запустить каждую функцию Фибоначчи на 100 000 итераций и в конце вывести, сколько времени потребовалось для завершения каждой из них.

Результаты могут различаться при разных запусках скрипта, но, как я вижу, порядок в моей системе одинаков (от самого быстрого к самому медленному): модуль Python C, функция Pure C, вызываемая из Python, функция Pure Python (выполняется с помощью обычного python команда).

Давайте также посмотрим, сколько времени потребуется для запуска чистой версии Python, если мы запустим ее с помощью PyPy:

Удивительно, но это даже быстрее, чем модуль, написанный на C. Однако я не думаю, что это справедливо для любого модуля, который вы можете написать, но для нашей простой функции fib это, кажется, так.

Надеюсь, эта информация оказалась для вас полезной, и спасибо за внимание!

Эта статья также размещена на моем собственном сайте здесь. Не стесняйтесь смотреть!