Инфраструктура и расширения программного обеспечения#
Реальные программы сложны и даже элементарный симулятор игральных костей требует большого количества кода. Чтобы упростить процесс, разработчики используют модульное программирование — разбивают задачи на более мелкие и более управляемые. Поэтому в ЯП так много модулей, пакетов, библиотек и фреймворков.
Условно можно обозначить их взаимоотношения с помощью этой диаграммы:
Модуль, пакет, библиотека, фреймворк#
В мире программирования существует целый набор терминов, которые описывают компоненты, используемые разработчиками для создания программного обеспечения. Среди них выделяются модули, пакеты, библиотеки и фреймворки, каждый из которых играет важную роль в процессе разработки.
Интерфейс#
Интерфейс — структура программы/синтаксиса, определяющая отношение с объектами, объединенными только некоторым поведением. При проектировании классов, разработка интерфейса тождественна разработке спецификации (множества методов, которые каждый класс, использующий интерфейс, должен реализовывать).
Интерфейс можно считать своеобразным договором между системой и внешним окружением. В рамках компьютерной программы «система» — рассматриваемая функция или модуль, а «окружение» — весь остальной проект. Интерфейс формально описывает, какие данные могут передаваться между системой и окружением. А «реализацию» можно охарактеризовать как «система минус интерфейс». В языках наподобие Haskell интерфейсы могут быть крайне специфическими. А в языках вроде Python они, напротив, очень обыденны.
Важнейшая концепция в разработке ПО — концепция интерфейса.
Модуль и Понятие абстракциии#
Модули в программировании представляют собой важную абстракцию, которая помогает организовать код, сделав его более структурированным, читаемым и повторно используемым. Эта концепция абстрагирует детали реализации, позволяя использовать функциональность модуля через определенный интерфейс, не углубляясь в специфики внутреннего устройства.
Info
Абстракция - это процесс скрытия деталей реализации и предоставления только необходимой информации. В контексте модулей это означает, что вам не нужно знать, как именно работает каждая часть модуля, чтобы использовать его функциональность. Вы можете использовать его интерфейс, предоставленный набором функций или классов, без погружения в детали его реализации.
Организация кода на модули позволяет логически разделить его на части, каждая из которых отвечает за определенную функциональность или задачу. Такая структурированность облегчает управление проектом и обеспечивает более эффективное обнаружение и редактирование необходимых участков программы.
Определяющей характеристикой модуля является скрытие информации. Модуль имеет интерфейс, который явно, но абстрактно определяет как функциональность, которую он предоставляет, так и функциональность, от которой он зависит. (Часто называют экспортируемые и импортируемые функции.) Этот интерфейс имеет реализацию (или, на самом деле, несколько реализаций), которые для пользователя модуля являются черным ящиком.
Важное преимущество модульного подхода – возможность повторного использования кода. Модули можно применять в различных частях программы или даже в разных проектах без необходимости переписывания функционала. Это способствует сокращению времени разработки и улучшает общую поддерживаемость кодовой базы.
В целом, модули представляют собой мощный инструмент, облегчающий понимание, организацию и повторное использование кода, что делает их важной составляющей при построении масштабируемых и поддерживаемых программных решений.
Достоинства модулей в том, что они:
- помогают сосредоточиться на одной небольшой части задаче;.
- сводят на нет дублирование за счет переиспользования;
- минимизируют вероятность конфликта имен.
Пакет#
Пакет — это коллекция связанных модулей, которые работают вместе для обеспечения определенной функциональности. Модули содержатся в папке и могут быть импортированы.
При разработке большого приложения легко столкнуться с множеством различных модулей, которыми сложно управлять. В таком случае полезно сгруппировать и упорядочить их с помощью пакетов.
Это каталоги модулей с иерархической структурой пространства имен. Точно так же, как мы раскладываем файлы на жестком диске по папкам и субдиректориям, можно организовать модули в пакеты и «подпакеты» (subpackages).
Пакеты позволяют создать иерархическую структуру пространства имен модулей. Точно так же, как мы организуем наши файлы на жестком диске в папки и
вложенные папки, мы можем организовать наши модули в пакеты и вложенные пакеты.
Примеры наиболее распространенных пакетов
- NumPy, который широко используется специалистами по обработке данных.
- Pandas, pandas — это пакет Python для быстрой и эффективной обработки табличных данных, временных рядов, матричных данных и т.д.
- Pytest, Pytest содержит модули, которые необходимы, когда вы хотите протестировать свой код.
Библиотеки#
Библиотека — это сборник модулей и пакетов, предназначенных для решения определенных задач.
Библиотеки могут быть как стандартными (входящими в поставку языка программирования), так и сторонними (разработанными внешними командами или сообществами разработчиков).
Виды библиотек:
1. По происхождению:
1.1 Стандартные библиотеки: Почти каждый язык программирования имеет свои стандартные библиотеки, которые поставляются вместе с языком и содержат базовые инструменты для работы с данными, вводом-выводом, математическими операциями и другими базовыми функциями.
1.2 Внешние библиотеки: Это библиотеки, разработанные сторонними компаниями или сообществами разработчиков, которые не входят в стандартную поставку языка, но предоставляют дополнительные возможности. Они могут включать в себя библиотеки для работы с графикой, сетями, базами данных, алгоритмами и многим другим.
-
По способу связывания:
2.1 Статические библиотеки: Включают код непосредственно в исполняемый файл программы на этапе компиляции. Код библиотеки статически копируется в программу и исполняется вместе с ней.
2.2 Динамические (разделяемые) библиотеки: Связываются с программой во время её выполнения. Код разделяемой библиотеки не копируется в исполняемый файл, а загружается в память при запуске программы.
Разделяемые библиотеки уменьшают объем кода, который дублируется в каждой программе, использующей библиотеку, сохраняя размер двоичных файлов небольшим. Это также позволяет вам заменить разделяемый объект функционально эквивалентным, но, возможно, дает дополнительные преимущества в производительности без необходимости перекомпиляции программы, которая его использует. Однако разделяемые библиотеки будут иметь небольшие дополнительные затраты на выполнение функций, а также на загрузку во время выполнения, поскольку все символы в библиотеке должны быть подключены к используемым ими устройствам. Кроме того, разделяемые библиотеки могут быть загружены в приложение во время выполнения, что является общим механизмом реализации бинарных подключаемых систем.
Статические библиотеки увеличивают общий размер двоичного файла, но это означает, что вам не нужно носить с собой копию используемой библиотеки. Поскольку код подключается во время компиляции, никаких дополнительных затрат на загрузку во время выполнения не требуется. Код просто есть.
Фреймворк#
Фреймворк - это комплексная структура, предоставляющая основы для разработки приложений. Он определяет архитектурные основы и правила, по которым должно строиться приложение. Фреймворки обычно содержат набор инструментов, библиотек и структур данных, которые упрощают разработку приложений, задавая общую структуру и правила работы.
Фреймворки «берут на себя» тысячи нюансов, например работу с файловой системой и базами данных, обработку ошибок, защиту программы.
Фреймворк» отличается от понятия библиотеки тем, что библиотека может быть использована в программном продукте просто как набор подпрограмм близкой функциональности, не влияя на архитектуру программного продукта и не накладывая на неё никаких ограничений
Сервис#
Сервисы — это дискретные программные компоненты, предоставляющие определенную функциональность и используемые в составе многих приложений
В отличии от библиотек и фреймворков, которые используется внутри программ, сервис является автономной сущностью по отношению к ней.
Понятие сервисов в программировании описывает дискретные программные компоненты, которые предоставляют определенную функциональность и могут использоваться в различных приложениях. Эти компоненты обычно создаются для выполнения специфических задач или предоставления конкретных сервисов для других частей программного обеспечения.
Основные характеристики сервисов:
- Функциональность: Сервисы выполняют определенные функции или задачи, предоставляя интерфейс для взаимодействия с другими компонентами программного обеспечения.
-
Независимость: Они могут быть разработаны таким образом, чтобы быть независимыми от других частей приложения, что обеспечивает гибкость и возможность их повторного использования.
-
Модульность: Сервисы часто структурированы в виде модулей или компонентов, что упрощает их управление и сопровождение.
-
Интерфейс: Обычно у сервисов есть четкий интерфейс, определяющий, как они могут быть использованы другими частями программы.
Примеры использования сервисов:
-
Аутентификационные сервисы: Предоставляют функциональность аутентификации и авторизации для многих приложений, обеспечивая безопасный доступ к данным и ресурсам.
-
Сервисы уведомлений: Предоставляют возможность отправки уведомлений или сообщений между различными системами или пользователями.
Использование сервисов позволяет разработчикам создавать более гибкие и модульные приложения, поскольку они могут использовать готовые компоненты для реализации определенных функций, а также упрощает масштабирование и поддержку программного обеспечения.
Дырявые абстракции#
Всё было бы слишком хорошо, если нам действительно не надо было бы никогда понимать внутрнеее устройства всех наших элементов. Иногда интерфейсы ведут себя непредсказуемым образом, поскольку части их внутренней реализации "вываливаются" наружу. Такое поведение было называно "Законом дырявых абстракций"
Как сформулировал Джоэл Сполски, Закон дырявых абстракций гласит:
Quote
Все нетривиальные абстракции, в той или иной степени, являются дырявыми.
Закон дырявых абстракций - это концепция в программировании, которая указывает на возможные проблемы при создании слишком абстрактных или не полностью абстрагированных уровней в программном коде.
Этот закон гласит, что когда вы создаете абстракцию (интерфейс, класс, функцию и т. д.), которая должна скрывать сложные детали реализации, но не полностью абстрагирует эти детали, то в результате у вас образуются "дыры" или утечки абстракции. Это означает, что часть сложности или деталей реализации все еще просачивается через вашу абстракцию, делая ее менее полезной или усложняя работу с ней.
К примеру, если уровень абстракции, такой как класс или интерфейс, скрывает множество деталей реализации, но при этом требует знания этих деталей для правильного использования, это может привести к сложностям для разработчиков, использующих данную абстракцию. Такие "дыры" в абстракции могут создавать путаницу, усложнять отладку или усложнять поддержку кода в будущем.
По мере усложнения систем разработчикам программного обеспечения приходится полагаться на большее количество абстракций. Каждая абстракция пытается скрыть сложность, позволяя разработчику писать программное обеспечение, которое "обрабатывает" множество вариаций современных вычислений.
Однако этот закон утверждает, что разработчики надежного программного обеспечения в любом случае должны изучить лежащие в основе абстракции детали.
Примеры дырявой абстракции#
В статье Джоэла Спольски, одном из примеров "дырявых абстракций", может быть приведен случай с базой данных. Предположим, что разработчик использует библиотеку или фреймворк, которые предоставляют абстракцию для работы с базой данных. Обычно эта абстракция работает отлично и скрывает детали взаимодействия с базой данных, предоставляя удобные методы для выполнения запросов и манипуляций данными.
Однако, если в какой-то момент потребуется выполнить сложный запрос или воспользоваться специфичной функциональностью базы данных, которая не была предусмотрена в абстракции, разработчик может столкнуться с необходимостью работать на более низком уровне - напрямую с базой данных. Это может потребовать знания специфических особенностей базы данных и обхода абстракции, что приводит к утечке сложности из абстракции в код, использующий эту абстракцию.
Таким образом, хотя абстракция базы данных может упрощать взаимодействие с данными и обеспечивать уровень абстракции, иногда неизбежно приходится использовать более прямой подход, который требует понимания внутреннего устройства базы данных и может повлечь за собой сложности и риски, связанные с управлением этой сложности на более низком уровне.
Создание модуля расширения для Python на языке Си#
Очень легко добавить в Python новые встроенные модули, если вы умеете программировать на C. Такие модули расширения могут делать две вещи, которые невозможно сделать непосредственно в Python: они могут реализовывать новые встроенные типы объектов, могут вызывать функции библиотеки C и системные вызовы.
Для поддержки расширений Python API (программный интерфейс приложения) определяет набор функций, макросов и переменных, которые обеспечивают доступ к большинству аспектов Python системы времени выполнения. API Python включается в исходный файл C путем включения "Python.h" заголовка.
Компиляция модуля расширения зависит от его предполагаемого использования, а также от настройки системы
Note
Интерфейс расширения C специфичен для CPython, и модули расширения не работают в других реализациях Python. Во многих случаях можно избежать записи расширений C и сохранить переносимость на другие реализации. Например, если используется вызов функций библиотеки C или системных вызовов, вместо записи пользовательского кода C следует использовать модуль ctypes или библиотеку cffi. Данные модули позволяют писать Python код с интерфейсом C кода и является более переносимыми между реализациями Python, чем запись и компиляция модуля расширения C.
C-расширения#
Интересной возможностью, которую предлагает разработчикам CPython, является
простота использования C-кода в Python.
Существует три метода, с помощью которых разработчик может вызвать C функцию из
Python кода - ctypes , SWIG и Python/C API .
У каждого метода есть свои
преимущества и недостатки.
Для начала, зачем нам вообще это может потребоваться?
Несколько популярных причин:
- Вам нужна скорость и вы знаете, что C в 50х раз быстрее Python
- Вам нужна конкретная C-библиотека и вы не хотите писать "велосипед" на Python
- Вам нужен низкоуровневый интерфейс управления ресурсами для работы с
памятью и файлами
- Просто потому что Вам так хочется
CTypes#
Модуль ctypes один из самых простых способов вызывать C-функции из Python. Он
предоставляет C-совместимые типы данных и функции для загрузки DLL, что
позволяет обращаться к библиотекам C без их модификации. Отсутствие
необходимости изменять C-код объясняет простоту данного метода.
Простой C-код для суммирования двух чисел, сохраните его как add.c
// Простой C-файл - суммируем целые и действительные числа
#include <stdio.h>
int add_int(int, int);
float add_float(float, float);
int add_int(int num1, int num2){
return num1 + num2;
}
float add_float(float num1, float num2){
return num1 + num2;
}
Теперь скомпилируем C-файл в .dll
-файл (.so
под Linux). Так мы получим файл
adder.dll
.
Для компиляции файла на C++ в динамическую библиотеку (.dll) в операционной системе Windows можно воспользоваться компилятором, предоставляемым, например, MinGW.
g++ -shared -o ИМЯ_ВАШЕГО_ФАЙЛА.dll МЯ_ВАШЕГО_ФАЙЛА.cpp
Что делать если minGW не установлен
Установочный файл GCC 11.4.0 + MinGW-w64 11.0.0 (UCRT) release 1 можно найти на этой (странице)[https://winlibs.com/#download-release].
Вы выбираете версию Win32 и Win64 в зависимости от своей системы.
Вы скачаете архив winlibs-x86_64-posix-seh-gcc-11.4.0-mingw-w64ucrt-11.0.0-r1.7z
Данные этого архива вытаскиваем. Внутри единственная папка minGW64
Её необходимо переместить в корень диска С (C:/minGW64)
Дальнейшие этапы смотрите в презентации: слайды 23-31
Откройте меню “Система” в “Панели управления”
Из меню “Система” перейдите в “Дополнительные параметры системы”
Выберите “Переменные среды”
Выберите переменную Path и нажмите кнопку “Изменить…”
Добавьте в новую строку полный путь до директории mingw32\bin и нажмите кнопку OK.
Чтобы проверить, что настройка выполнена успешно, откройте консоль (не в директории mingw32\bin) и выполните команду g++ --help:
Теперь мы можем легко подключить наш файл и вызвать функцию
from ctypes import *
# Загружаем библиотеку
adder = CDLL('./adder.dll')
# Находим сумму целых чисел
res_int = adder.add_int(4,5)
print("Сумма 4 и 5 = " + str(res_int))
# Находим сумму действительных чисел
a = c_float(5.5)
b = c_float(4.1)
add_float = adder.add_float
add_float.restype = c_float
print("Сумма 5.5 и 4.1 = " + str(add_float(a, b)))
В примере выше, C-файл содержит простой код - две функции: одна для нахождения
суммы двух целых чисел, другая - действительных.
В Python-коде мы сначала импортируем модуль ctypes . Затем функция CDLL из того
же модуля используется для загрузки C-библиотеки. Теперь функции из C-кода
доступны для нас через переменную adder . Когда мы вызываем adder.add_int() , то
автоматически вызывается C-функция add_int . Интерфейс модуля ctypes позволяет
использовать питоновские целые числа и строки при вызове C-функций.
Для других типов, например логического или действительных чисел, мы должны
использовать корректные ctypes .
Мы делаем это при передаче параметров в
adder.add_float() . Сначала мы создаём требуемый тип c_float из float , затем
используем в качестве аргумента для C-кода. Этот метод простой и аккуратный, но
ограниченный. Мы, к примеру, не можем оперировать объектами на стороне C-кода.
SWIG#
SWIG (Simplified Wrapper and Interface Generator) - это инструмент, который помогает связывать код на C/C++ с другими языками программирования высокого уровня, такими как Python, Java, C#, Ruby, и другими. Он упрощает создание оболочек (wrappers), которые позволяют использовать функциональность, написанную на C/C++, из других языков.
В этом методе разработчик должен написать отдельный файл, описывающий интерфейс, который будет передаваться в SWIG (утилиту командной строки).
Python-разработчики обычно не используют данный подход, поскольку в большинстве
случаев он неоправданно сложен. Тем не менее, это отличный вариант, когда у вас
есть C/C++ код, к которому нужно обращаться из множества различных языков.
SWIG полезен, если вам нужно использовать библиотеку или функциональность, написанную на C/C++, в вашем проекте на другом языке программирования. Он упрощает процесс интеграции, позволяя вам использовать существующий код без необходимости переписывать его с нуля для каждого языка.
#include <time.h>
double My_variable = 3.0;
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
int my_mod(int x, int y) {
return (x%y);
}
char *get_time()
{
time_t ltime;
time(<ime);
return ctime(<ime);
}
Файл, описывающий интерфейс. Он не будет изменяться в зависимости от языка, на
который вы хотите портировать свой C-код
/* example.i */
%module example
%{
/* Помещаем сюда заголовочные файлы или объявления функций */
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
%}
extern double My_variable;
extern int fact(int n);
extern int my_mod(int x, int y);
extern char *get_time();
Компиляция:
unix % swig -python example.i
unix % gcc -c example.c example_wrap.c \
-I/usr/local/include/python2.1
unix % ld -shared example.o example_wrap.o -o _example.so
Однако порой труды стоят того, поскольку теперь осуществить импорт будет крайне легко:
>>> import example
>>> example.fact(5)
120
>>> example.my_mod(7,3)
1
>>> example.get_time()
'Sun Feb 11 23:01:07 1996'
>>>
Как мы можем видеть, SWIG позволяет добиваться нужного нам эффекта, но он
требует дополнительных усилий, которые, однако, стоит затратить, если вас
интересует возможность запуска C-кода из множества различных языков.
Python/C API#
C/Python API это, вероятно, наиболее широко применяемый метод - не благодаря
своей простоте, а потому что он позволяет оперировать Python объектами из C кода.
Этот метод подразумевает написание C-кода специально для работы с Python. Все
объекты Python представляются как структуры PyObject и заголовочный файл
Python.h предоставляет различные функции для работы с объектами. Например,
если PyObject одновременно PyListType (список), то мы можем использовать
функцию PyList_Size() , чтобы получить длину списка. Это эквивалентно коду
len(some_list) в Python. Большинство основных функций/операторов для
стандартных Python объектов доступны в C через Python.h .
Давайте напишем С-библиотеку для суммирования всех элементов списка Python (все
элементы являются числами).
Начнем с интерфейса, который мы хотим иметь в итоге. Вот Python-файл,
использующий пока отсутствующую C-библиотеку:
# Это не простой Python import, addList это C-библиотека
import addList
l = [1,2,3,4,5]
print("Сумма элементов списка - " + str(l) + " = " + str(addList.add(l)))
Смотрится как обыкновенный Python-код, который импортирует и использует Pythonмодуль addList . Единственная разница - модуль addList написан на C.
Python C расширения.
Дальше на повестке у нас C-код, который будет встроен в Python-модуль addList , это
может смотреться немного странно, однако, разобрав отдельные части, из которых
состоит C-файл, вы увидите, что все относительно незатейливо.
// Python.h содержит все необходимые функции, для работы с объектами Python
#include <Python.h>
// Эту функцию мы вызываем из Python кода
static PyObject* addList_add(PyObject* self, PyObject* args){
PyObject * listObj;
// Входящие аргументы находятся в кортеже
// В нашем случае есть только один аргумент - список, на который мы будем
// ссылаться как listObj
if (! PyArg_ParseTuple( args, "O", &listObj))
return NULL;
// Длина списка
long length = PyList_Size(listObj);
// Проходимся по всем элементам
int i, sum =0;
for(i = 0; i < length; i++){
// Получаем элемент из списка - он также Python-объект
PyObject* temp = PyList_GetItem(listObj, i);
// Мы знаем, что элемент это целое число - приводим его к типу C long
long elem = PyInt_AsLong(temp);
sum += elem;
}
// Возвращаемое в Python-код значение также Python-объект
// Приводим C long к Python integer
return Py_BuildValue("i", sum);
}
// Немного документации для `add`
static char addList_docs[] =
"add( ): add all elements of the list\n";
/*
Эта таблица содержит необходимую информацию о функциях модуля
<имя функции в модуле Python>, <фактическая функция>,
<ожидаемые типы аргументов функции>, <документация функции>
*/
static PyMethodDef addList_funcs[] = {
{"add", (PyCFunction)addList_add, METH_VARARGS, addList_docs},
{NULL, NULL, 0, NULL}
};
/*
addList имя модуля и это блок его инициализации.
<желаемое имя модуля>, <таблица информации>, <документация модуля>
*/
PyMODINIT_FUNC initaddList(void){
Py_InitModule3("addList", addList_funcs,
"Add all ze lists");
}
Пошаговое объяснение:
- Заголовочный файл
содержит все требуемые типы (для
представления типов объектов в Python) и определения функций (для работы с
Python-объектами). - Дальше мы пишем функцию, которую собираемся вызывать из Python. По
соглашению, имя функции принимается {module-name}_{function-name}, которое в
нашем случае - addList_add . Подробнее об этой функции будет дальше. - Затем заполняем таблицу, которая содержит всю необходимую информацию о
функциях, которые мы хотим иметь в модуле. Каждая строка относится к функции,
последняя - контрольное значение (строка из null элементов). - Затем идёт блок инициализации модуля - PyMODINIT_FUNC init{module-name} .
Функция addList_add принимает аргументы типа PyObject (args также является
кортежем, но поскольку в Python всё является объектами, мы используем
унифицированный тип PyObject ). Мы парсим входные аргументы (фактически,
разбиваем кортеж на отдельные элементы) при помощи PyArg_ParseTuple() . Первый
параметр является аргументом для парсинга. Второй аргумент - строка,
регламентирующая процесс парсинга элементов кортежа args. Знак на N-ой позиции
строки сообщает нам тип N-ого элемента кортежа args, например - 'i' значит integer, 's' -
строка и 'O' - Python-объект. Затем следует несколько аргументов, где мы хотели бы
хранить выходные элементы PyArg_ParseTuple() . Число этих аргументов равно числу
аргументов, которые планируется передавать в функцию модуля и их позиционность
должна соблюдаться
Например, если мы ожидаем строку, целое число и список в
таком порядке, сигнатура функции будет следующего вида:
int n;
char *s;
PyObject* list;
PyArg_ParseTuple(args, "siO", &n, &s, &list);
В данном случае, нам нужно извлечь только объект списка и сохранить его в
переменной listObj . Затем мы используем функцию PyList_Size() чтобы получить
длину списка. Логика совпадает с len(some_list) в Python.
Теперь мы итерируем по списку, получая элементы при помощи функции
PyLint_GetItem(list, index) . Так мы получаем PyObject*. Однако, поскольку мы знаем,
что Python-объекты еще и PyIntType , то используем функцию PyInt_AsLong(PyObj *)
для получения значения. Выполняем процедуру для каждого элемента и получаем
сумму.
Сумма преобразуется в Python-объект и возвращается в Python-код при помощи
Py_BuildValue() . Аргумент "i" означает, что возвращаемое значение имеет тип integer.
В заключение мы собираем C-модуль. Сохраните следующий код как файл setup.py :
# Собираем модули
from distutils.core import setup, Extension
setup(name='addList', version='1.0',\
ext_modules=[Extension('addList', ['adder.c'])])
и запустите:
python setup.py install
Это соберёт и установит C-файл в Python-модуль, который нам требуется.
Теперь осталось только протестировать работоспособность:
# Модуль, вызывающий C-код
import addList
l = [1,2,3,4,5]
print("Сумма элементов списка - " + str(l) + " = " + str(addList.add(l)))
В итоге, как вы можете видеть, мы получили наше первое C-расширение,
использующее Python.h API. Этот метод может показаться сложным, однако с
практикой вы поймёте его удобство.
Из других методов встраивания C-кода в Python, можно отметить альтернативный и
быстрый компилятор Cython. Однако Cython, по сути, отличный от основной ветки
Python язык, поэтому я не стал здесь его рассматривать.