В этой статье поговорим о том, как можно ускорить свой Python код при помощи библиотек, скомпилированных с помощью Nim.
Также узнаем, какие библиотеки на Python написаны с помощью Nim и даже напишем свой небольшой модуль.

Подготовка
Для того, чтобы начать - необходимо поставить Nim. Если он у вас уже есть - отлично, идем дальше.
Все действия ниже будут производиться с Nim 2.0.0 и с Python 3.10.9.
С помощью пакетного менеджера nimble ставим пакет nimpy, с помощью которого мы сможем разрабатывать Python библиотеки на Nim.
nimble install nimpy
Переходим к Python и возьмем какой-нибудь алгоритм для замера производительности, например фибоначчи. Создадим файл fib.py и напишем саму функцию.
def fib(n: int) -> int: if n == 0: return 0 elif n < 3: return 1 return fib(n - 1) + fib(n - 2)
Теперь вернемся к Nim и создадим файл nimfib.nim. Как это будет выглядеть здесь?
import nimpy # импортируем библиотеку nimpy # Объявляем функцию fib(n) func fib(n: int): int {.exportpy.} = if n == 0: return 0 elif n < 3: return 1 return fib(n - 1) + fib(n - 2)
Выглядит действительно схоже, не так ли? Попробуем скомпилировать в python библиотеку:
nim c -o:nimfib.pyd --tlsEmulation:off --passL:-static --threads:on --app:lib -d:danger --opt:speed nimfib
Эту команду можно вынести в отдельный файл.
Для Unix систем команда выше выглядит следующим образом:
nim c -o:nimfib.so --app:lib -d:danger --threads:on --opt:speed nimfib
При компиляции с помощью
--threads:onNim будет подставлять--tlsEmulation:on(только для Windows), что предотвращает правильную инициализацию среды выполнения Nim при вызове из внешнего потока (что всегда имеет место в случае модуля Python).
Теперь посмотрим, насколько быстро работают Nim и Python. Для этого ставим пакеты pytest и pytest-benchmark.
pip install pytest pytest-benchmark
Создадим файл main.py:
from timeit import default_timer import pytest import fib import nimfib @pytest.mark.benchmark(group="fibonacci", timer=default_timer) def test_py_fib(benchmark): result = benchmark(fib.fib, 35) @pytest.mark.benchmark(group="fibonacci", timer=default_timer) def test_nim_fib(benchmark): result = benchmark(nimfib.fib, 35)
Теперь запустим это через pytest:
pytest main.py
А вот и результаты:
--------------------------------------------------------------------------------- benchmark 'fibonacci': 2 tests --------------------------------------------------------------------------------- Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- test_nim_fib 93.4353 (1.0) 117.2837 (1.0) 102.3561 (1.0) 7.9790 (1.0) 99.2676 (1.0) 11.3069 (1.0) 3;0 9.7698 (1.0) 9 1 test_py_fib 2,986.7212 (31.97) 3,013.6137 (25.70) 2,998.6946 (29.30) 11.7603 (1.47) 3,001.7616 (30.24) 20.1307 (1.78) 3;0 0.3335 (0.03) 5 1 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Legend: Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. OPS: Operations Per Second, computed as 1 / Mean ====================================================================== 2 passed in 23.19s =======================================================================
Как вы можете видеть - Nim, скомпилированный в C быстрее Python в 30 раз.
Если мы скомпилируем Nim в C++ (заменив nim c на nim cpp), то получим уже следующие результаты:
--------------------------------------------------------------------------------- benchmark 'fibonacci': 2 tests --------------------------------------------------------------------------------- Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- test_nim_fib 9.4439 (1.0) 14.0821 (1.0) 9.7844 (1.0) 0.7489 (1.0) 9.5708 (1.0) 0.1119 (1.0) 7;15 102.2032 (1.0) 106 1 test_py_fib 3,003.1009 (317.99) 3,016.5476 (214.21) 3,009.6761 (307.60) 5.4503 (7.28) 3,010.4404 (314.55) 8.8961 (79.50) 2;0 0.3323 (0.00) 5 1 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Legend: Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile. OPS: Operations Per Second, computed as 1 / Mean ====================================================================== 2 passed in 23.19s =======================================================================
Разница в 300 раз, действительно впечатляет. Конечно, вы можете ускорить его еще больше, если хотите - у Nim достаточно параметров для компиляции.
Перейдем к более реальным примерам
Готовые библиотеки
Есть несколько реальных примеров использования nimpy для разработки Python библиотек:
faster-than-requests (исходный код) - тот же requests, но написан на Nim.
faster-than-csv (исходный код) - тот же csv, но на Nim.
nimporter (исходный код) - утилита, с помощью которой можно импортировать Nim файлы напрямую в Python. Компиляция происходит автоматически.
HappyX (исходный код) - веб-фреймворк, написанный на Nim и доступный как для Python, так и для NodeJS и JVM.
pyMeow (исходный код) - библиотека для написания читов с помощью Raylib, Python и Nim.
Кошки, собаки и Python классы
Давайте попробуем создать на стороне Nim объект Entity. Создадим файл entity.nim:
import nimpy import strformat type Entity* = ref object of PyNimObjectExperimental health: int maxHealth: int damage: int name: string proc initEntity*(name: string, health: int, damage: int = 1): Entity {.exportpy: "create_entity".} = return Entity( name: name, health: health, maxHealth: health, damage: damage ) proc isAlive*(self: Entity): bool {.exportpy: "is_alive".} = return self.health > 0 proc hit*(self: Entity, other: Entity) {.exportpy.} = # Примитивная логика получения удара other.health -= self.damage if other.health <= 0: echo fmt"{other.name} погиб в бою от руки {self.name} ?" else: echo fmt"{other.name} получает {self.damage} урона от {self.name} ⚔" proc name*(self: Entity): string {.exportpy.} = return self.name
Теперь скомпилируем:
nim c -o:entity.pyd --tlsEmulation:off --passL:-static --app:lib entity
И попробуем в Python:
import entity dog = entity.create_entity("Собака", 10) cat = entity.create_entity("Кот", 4, 2) while dog.is_alive() and cat.is_alive(): dog.hit(cat) cat.hit(dog) if dog.is_alive(): print(f"{dog.name()} одержал победу!") else: print(f"{cat.name()} одержал победу!")
Как можно заметить - при создании собаки мы отправили лишь 2 аргумента из трех, потому что в create_entity у аргумента damage есть значение по умолчанию.
Запускаем и видим результат:
Кот получает 1 урона от Собака ⚔ Собака получает 2 урона от Кот ⚔ Ко�� получает 1 урона от Собака ⚔ Собака получает 2 урона от Кот ⚔ Кот получает 1 урона от Собака ⚔ Собака получает 2 урона от Кот ⚔ Кот погиб в бою от руки Собака ? Собака получает 2 урона от Кот ⚔ Собака одержал победу!
Заключение
Да, с помощью Nim можно создавать модули для Python, однако нужно понимать, что не для всего подойдет расширение на C/C++. В основном это какие-то низкоуровневые операции, работа с потоками и прочие cpu-bound вычисления.
