Nuitka: компиляция Python-кода в C
От упаковки к компиляции: как Nuitka переводит программу в C, что становится машинным кодом, а что остаётся PyObject — и где ждать ускорения.
Редакция Питоники
PyInstaller упаковывает: ваш код внутри exe остаётся байткодом, и исполняет его всё та же виртуальная машина. Nuitka идёт дальше — это компилятор: программа транслируется в C, C собирается в машинный код, и интерпретация байткода с горячего пути исчезает. Урок разбирает конвейер компиляции по шагам и показывает на чистом Python, за счёт чего именно появляется ускорение.
Два режима работы
У Nuitka два режима. accelerated компилирует модуль в C-расширение — файл .pyd на Windows или .so на Unix: такой модуль импортируется обычным Python и ускоряет только себя, но требует установленного интерпретатора. standalone собирает полноценное приложение с библиотеками — как PyInstaller, только код внутри нативный:
| Режим | На выходе | Python на машине пользователя |
|---|---|---|
| accelerated | C-расширение .pyd или .so | Нужен |
| standalone | Папка или один exe с библиотеками | Не нужен |
Установка и сборка
pip install nuitka
python -m nuitka main.py
Для сборки нужен C-компилятор: MSVC или MinGW на Windows, gcc или clang на Unix-системах. Сборка занимает заметно больше времени, чем у PyInstaller, — это цена настоящего компилирования.
Что значит «компиляция в C»
Конвейер такой: исходник разбирается в дерево AST, из дерева Nuitka строит своё представление программы, из него генерирует C-код, и компилятор C доводит до машинного. Первый шаг можно повторить прямо в песочнице — стандартный модуль ast разбирает тот же исходник:
import ast
source = (
"def total(prices):\n"
" s = 0\n"
" for p in prices:\n"
" s = s + p\n"
" return s\n"
)
tree = ast.parse(source)
func = tree.body[0]
print('Функция:', func.name)
print('Аргументы:', [a.arg for a in func.args.args])
kinds = []
for node in ast.walk(func):
if isinstance(node, (ast.Assign, ast.For, ast.BinOp, ast.Return)):
kinds.append(type(node).__name__)
print('Конструкции:', ', '.join(kinds))
Функция: total Аргументы: ['prices'] Конструкции: Assign, For, Return, Assign, BinOp
Перед компилятором лежит дерево: функция, присваивания, цикл, сложение. Теперь он может делать то, что интерпретатор не умеет, — оптимизировать по всей программе. Классика: свёртка констант — выражения из одних констант вычисляются ещё при сборке, а не при каждом запуске:
import ast
class Folder(ast.NodeTransformer):
def visit_BinOp(self, node):
self.generic_visit(node)
ops = {ast.Add: lambda a, b: a + b, ast.Sub: lambda a, b: a - b, ast.Mult: lambda a, b: a * b}
if (
isinstance(node.left, ast.Constant)
and isinstance(node.right, ast.Constant)
and type(node.op) in ops
):
return ast.Constant(value=ops[type(node.op)](node.left.value, node.right.value))
return node
print('Было: n = 2 + 3 * 4')
folded = Folder().visit(ast.parse('n = 2 + 3 * 4'))
ast.fix_missing_locations(folded)
ns = {}
exec(compile(folded, '<folded>', 'exec'), ns)
print('Стало: n =', ns['n'])
Было: n = 2 + 3 * 4 Стало: n = 14
Выражение вычислилось при сборке: в машинном коде лежит готовое значение. Таких оптимизаций у Nuitka десятки — от свёртки констант до упрощения известных форм вызовов.
/* упрощённая иллюстрация, а не реальный вывод Nuitka */
static PyObject *impl_total(PyObject *self, PyObject *prices) {
PyObject *s = PyLong_FromLong(0L);
/* цикл for p in prices стал машинным циклом */
/* s = s + p стал вызовом PyNumber_Add(s, p) */
return s;
}
Что ускоряется, а что нет
Ключ к честным ожиданиям: компиляция не превращает Python в C с машинными числами. Значения остаются теми же объектами — PyObject, и арифметика над ними идёт через те же функции CPython. Проверим это на функции в «компиляторном стиле»:
def compiled_style(a, b):
s = a + b
return s * 2
result = compiled_style(21, 21)
print('Результат:', result)
print('Тип результата:', type(result))
Результат: 84 Тип результата: <class 'int'>
Nuitka заменяет интерпретацию байткода машинным потоком управления. Числа при этом остаются объектами CPython: ускоряются циклы и вызовы, а не каждая отдельная операция. Отсюда правило: выигрывает код с плотными циклами, ветвлениями и вызовами — а не одинокая арифметика.
Два источника ускорения
Первый источник: код собирается один раз. CPython разбирает и компилирует исходник при каждом запуске программы (пусть и с кэшем pyc), а нативный код собран заранее. Измерим саму идею: разбор строки на каждый вызов против заранее собранного кода:
import time
source = 'a * a + 2 * b'
ctx = {'a': 7, 'b': 30}
precompiled = compile(source, '<expr>', 'eval')
n = 10_000
t0 = time.perf_counter()
for _ in range(n):
eval(compile(source, '<expr>', 'eval'), ctx)
t_fresh = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(n):
eval(precompiled, ctx)
t_ready = time.perf_counter() - t0
print('Результат одинаковый:', eval(precompiled, ctx) == eval(compile(source, '<expr>', 'eval'), ctx))
print('Скомпилированный заранее быстрее:', t_ready < t_fresh)
Результат одинаковый: True Скомпилированный заранее быстрее: True
Второй источник: исчезает шаговая интерпретация. Виртуальная машина выполняет программу по инструкциям: диспетчер читает опкод, выбирает ветку, исполняет. Прямой код идёт без диспетчера. Сравним вызов функции со «стековой машиной» — так изнутри выглядит интерпретация:
import time
def direct(a, b, c):
return (a + b) * c
def vm(program, args):
stack = []
for op, arg in program:
if op == 'push':
stack.append(args[arg])
elif op == 'add':
y = stack.pop()
x = stack.pop()
stack.append(x + y)
elif op == 'mul':
y = stack.pop()
x = stack.pop()
stack.append(x * y)
elif op == 'ret':
return stack[0]
prog = [('push', 0), ('push', 1), ('add', None), ('push', 2), ('mul', None), ('ret', None)]
args = (7, 5, 3)
print('Результат совпадает:', vm(prog, args) == direct(7, 5, 3))
n = 30_000
t0 = time.perf_counter()
for _ in range(n):
direct(7, 5, 3)
t_direct = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(n):
vm(prog, args)
t_vm = time.perf_counter() - t0
print('Прямой код быстрее стековой машины:', t_direct < t_vm)
Результат совпадает: True Прямой код быстрее стековой машины: True
Обе разницы мы выразили сравнением больше-меньше, а не абсолютными миллисекундами: время зависит от машины, а порядок результатов — никогда. Нативный код получает оба выигрыша разом: собран заранее и исполняется без диспетчера.
Nuitka против PyInstaller
| Критерий | PyInstaller | Nuitka |
|---|---|---|
| Что с кодом | Байткод на виртуальной машине | Машинный код |
| Скорость исполнения | Как у обычного Python | Выше на плотном коде |
| Сборка | Быстро, компилятор не нужен | Нужен C-компилятор, дольше |
| Совместимость | Почти всё | Редкие несовместимости с динамикой |
| Суть | Упаковка | Компиляция |
python -m nuitka --standalone --onefile main.py
Для точечного ускорения вместо компиляции всей программы существует хирургический инструмент: Cython добавляет к Python типы и компилирует только горячий модуль — об этом следующий урок.
Сначала предскажи ответ в голове — это главный навык программиста.
import ast
class Folder(ast.NodeTransformer):
def visit_BinOp(self, node):
self.generic_visit(node)
ops = {ast.Add: lambda a, b: a + b, ast.Mult: lambda a, b: a * b}
if (
isinstance(node.left, ast.Constant)
and isinstance(node.right, ast.Constant)
and type(node.op) in ops
):
return ast.Constant(value=ops[type(node.op)](node.left.value, node.right.value))
return node
ns = {}
folded = Folder().visit(ast.parse('n = 5 + 2 * 10'))
ast.fix_missing_locations(folded)
exec(compile(folded, '<folded>', 'exec'), ns)
print('n =', ns['n'])
import ast
tree = ast.parse('def f(x):\n return x + 1\n')
func = tree.body[0]
print('Функция:', func.name)
kinds = [type(n).__name__ for n in ast.walk(func) if isinstance(n, (ast.BinOp, ast.Return))]
print('Конструкции:', ', '.join(kinds))
def direct(a, b):
return (a + b) * 2
def vm(program, args):
stack = []
for op, arg in program:
if op == 'push':
stack.append(args[arg])
elif op == 'add':
y = stack.pop()
stack.append(stack.pop() + y)
elif op == 'mul':
y = stack.pop()
stack.append(stack.pop() * y)
elif op == 'ret':
return stack[0]
prog = [('push', 0), ('push', 1), ('add', None), ('push', 2), ('mul', None), ('ret', None)]
print(vm(prog, (5, 4, 2)) == direct(5, 4))
1. Что делает Nuitka?
2. Чем режим accelerated отличается от standalone?
3. Что нужно для сборки Nuitka?
4. Почему компиляция не даёт «машинных чисел» как в настоящем C?
5. Что такое свёртка констант?
6. Какой код может вести себя иначе после нативной компиляции?
7. Главное различие PyInstaller и Nuitka?
Сравни «компиляцию на каждый вызов» и «заранее собранный код»: два цикла по 1000 вызовов eval — первый каждый раз компилирует строку, второй использует готовый code-объект. Напечатай два булевых: одинаковый результат и заранее собранный быстрее.
Что такое Nuitka простыми словами?
Компилятор, который переводит вашу Python-программу в C и собирает из неё настоящий исполняемый файл. Код перестаёт исполняться по инструкциям виртуальной машины — машинный код идёт напрямую, поэтому плотные циклы и вызовы ускоряются.
Урок 10: PyInstaller — упаковка в exeNuitka или PyInstaller — что выбрать?
PyInstaller — когда нужна простая и быстрая упаковка: байткод и интерпретатор в exe без компилятора. Nuitka — когда важна скорость исполнения или сокрытие кода: сборка дольше, нужен C-компилятор, но код становится нативным.
Нужно ли переписывать код под Nuitka?
Обычно нет: Nuitka компилирует обычный Python. Проверять стоит глубоко рефлексивный код — инспекцию фреймов, подмену байткода, трюки с globals. Правило: сначала тесты на собранной сборке, потом релиз.
Зачем Cython, если есть Nuitka?
Cython — хирургия: вы добавляете типы и компилируете один горячий модуль в C-расширение, остальная программа остаётся обычным Python. Nuitka компилирует всё целиком. Точечное ускорение — Cython, вся программа — Nuitka.
Урок 13: Cython — типизация ради скоростиПонравился урок? Сошлитесь на него
«Числа при этом остаются объектами CPython: ускоряются циклы и вызовы, а не каждая отдельная операция.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 10
PyInstaller: сборка exe из Python-скрипта
От скрипта к exe: PyInstaller анализирует импорты, кладёт интерпретатор, байткод и данные в один файл. Собираем бандл по шагам и проверяем его на чистой машине.
Компиляция · Урок 13
Cython: типизация ради скорости
Хирургия вместо компиляции всей программы: cdef-типы превращают горячий модуль в C-расширение. Где типы дают ускорение, а где не дают ничего — с замерами на чистом Python.
Компиляция · Урок 15
Декомпиляция pyc в Python: что видно из байткода
pyc не содержит исходного текста, но хранит имена, константы и структуру. Разбираем, что восстановит декомпилятор, что потеряно навсегда и где проходит этическая граница.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Исключения · Урок 18
assert в Python: проверки для разработчика и AssertionError
assert — это заявление: здесь должно быть так. Провал превращается в AssertionError с вашим сообщением, а флаг -O умеет вырезать все проверки разом — поэтому инструмент разработчика, а не валидатор ввода.
assert против raise python
requests · Урок 4
Ответ requests: text, content и json — три лица ответа
У ответа requests три лица: .text — строка, .content — байты, .json() — готовый объект Python. Разбираемся, когда какое брать и откуда берутся кракозябры.
str против bytes python
Компиляция · Урок 3
py_compile и compileall: компилируем Python сами
Два инструмента стандартной библиотеки компилируют код без запуска: py_compile — один файл, compileall — целый каталог. Собираем pyc вручную и ловим ошибки компиляции.
python компиляция файлакомпиляция модулей python