Cython: типизация ради скорости
Хирургия вместо компиляции всей программы: cdef-типы превращают горячий модуль в C-расширение. Где типы дают ускорение, а где не дают ничего — с замерами на чистом Python.
Редакция Питоники
Nuitka из прошлого урока компилирует всю программу. Cython — инструмент точнее: это надмножество Python, в которое добавляют типы, и компилируют не программу, а отдельный горячий модуль в C-расширение. Остальной код остаётся обычным Python и просто импортирует ускоренный модуль. Урок показывает механику типизации на живом коде: где типы меняют исполнение, а где остаются украшением.
Чем Cython отличается от Nuitka
Nuitka берёт программу как есть и сохраняет семантику Python автоматически — писать типы не нужно. Cython переворачивает договор: вы сами объявляете типы там, где они важны, и за это получаете настоящий машинный код в этих местах. Масштаб другой: Cython собирает модули, Nuitka — программы. Сборка exe делается поверх — например, PyInstaller подхватывает Cython-расширение как бинарник.
Как выглядит Cython-код
# fastmath.pyx — исходник Cython
def total(prices):
cdef double s = 0.0 # C-переменная: без объектов на каждом шаге
for price in prices:
s += price
return s
Строка cdef double s меняет всё: переменная становится машинным числом двойной точности — на каждом шаге цикла нет создания объекта, проверки типа и поиска атрибутов. Именно cdef-объявления дают ускорение. А обычные аннотации — нет: в чистом Python они хранятся как данные и никогда не проверяются:
def total(prices: 'list[float]') -> float:
s = 0.0
for p in prices:
s += p
return s
print(total([100, 250, 80]))
print('Аннотации:', total.__annotations__)
430.0
Аннотации: {'prices': 'list[float]', 'return': <class 'float'>}Функция отработала с целыми числами, хотя аннотация обещала float: интерпретатор хранит аннотации в словаре __annotations__ и не сверяет с ними ни один вызов. Их адресат — инструменты: Cython (в том числе в pure-Python режиме) превращает аннотации в типы при компиляции, а без компиляции они остаются записью.
Типы на границе: что пропускает cdef
Типизированная функция Cython на границе преобразует числа и отбрасывает нечисловые значения с TypeError. Поведение легко воспроизвести проверкой типов:
def strict_double(x):
if not isinstance(x, float) and not isinstance(x, int):
raise TypeError('Ожидалось число')
return x * 2
print(strict_double(2.5))
print(strict_double(4))
try:
strict_double('много')
except TypeError as e:
print('Ошибка типа:', e)
5.0 8 Ошибка типа: Ожидалось число
Числа прошли — int превращается в double без шума, строка отброшена. Это и есть контракт типизированной функции: принимаю числа, не принимаю остальное, и проверка происходит при входе, а не в глубине цикла.
Сборка расширения
pip install cython setuptools
# setup.py:
# from setuptools import setup
# from Cython.Build import cythonize
# setup(ext_modules=cythonize('fastmath.pyx'))
python setup.py build_ext --inplace
После сборки import fastmath работает как обычный модуль — только функция внутри нативная. Проверять ускорение принято модулей timeit на реальной машине:
python -m timeit -s "from fastmath import total" "total(list(range(1000)))"
Где типы дают выигрыш
Главный приём — типизированная переменная вместо объекта на каждом шаге цикла. Ближайший родственник в стандартной библиотеке — модуль array: он хранит числа как машинные double без обёрток. Сравним цикл по обычному списку объектов с суммой по типизированному массиву:
from array import array
import time
def total_loop(values):
s = 0.0
for v in values:
s += v
return s
data_obj = [i * 0.5 for i in range(50_000)]
data_arr = array('d', data_obj)
t0 = time.perf_counter()
for _ in range(30):
total_loop(data_obj)
t_loop = time.perf_counter() - t0
t0 = time.perf_counter()
for _ in range(30):
sum(data_arr)
t_arr = time.perf_counter() - t0
print('Ответ тот же:', sum(data_arr) == total_loop(data_obj))
print('Массив быстрее объектного цикла:', t_arr < t_loop)
Ответ тот же: True Массив быстрее объектного цикла: True
Разницу мы выразили сравнением больше-меньше, а не абсолютными числами: миллисекунды зависят от машины и версии, а порядок результатов — никогда. Второй приём Cython — вынести поиск атрибута из цикла в локальную переменную: для cdef-объявлений он делает это автоматически:
import time
class Ledger:
def __init__(self):
self.total = 0
def update_dynamic(ledger, steps):
for i in range(steps):
ledger.total += i
def update_typed(ledger, steps):
total = ledger.total
for i in range(steps):
total += i
ledger.total = total
a = Ledger()
b = Ledger()
update_dynamic(a, 1_000)
update_typed(b, 1_000)
print('Результаты совпадают:', a.total == b.total)
t0 = time.perf_counter()
update_dynamic(Ledger(), 50_000)
t_dyn = time.perf_counter() - t0
t0 = time.perf_counter()
update_typed(Ledger(), 50_000)
t_typ = time.perf_counter() - t0
print('Локальная переменная быстрее атрибута:', t_typ < t_dyn)
Результаты совпадают: True Локальная переменная быстрее атрибута: True
Сначала профиль, потом типы
Типизируют не всё подряд, а горячее — то, что съедает время. Найти горячее — работа профилировщика cProfile: соберём профиль двух функций и посмотрим, где программа провела время:
import cProfile, pstats
def loop_total():
total = 0
for i in range(300_000):
total += i
return total
def quick_sort():
return sorted([3, 1, 2])
profiler = cProfile.Profile()
profiler.enable()
quick_sort()
loop_total()
profiler.disable()
stats = pstats.Stats(profiler)
times = {}
for key, value in stats.stats.items():
if key[2] in ('loop_total', 'quick_sort'):
times[key[2]] = value[3]
hotter = max(times, key=times.get)
print('Обе функции отработали')
print('Компилировать стоит:', hotter)
print('Горячая дольше холодной:', times['loop_total'] > times['quick_sort'])
Обе функции отработали Компилировать стоит: loop_total Горячая дольше холодной: True
Почти всё время — в цикле: его и типизируют. Сортировку трёх элементов компилировать не стоит. Правило простое: сначала профиль, потом cdef — ускорение функции, на которую ушла доля процента времени, никто не заметит.
Скомпилированное расширение — бинарник с платформенным именем: в exe-сборке PyInstaller подхватывает его как библиотеку, а исходники .pyx пользователю не нужны. Как альтернатива всего-сразу — Nuitka, как другие упаковщики — py2exe и cx_Freeze в следующем уроке.
Сначала предскажи ответ в голове — это главный навык программиста.
def scale(x: 'float') -> float:
return x * 3
print(scale(5))
print('Аннотации:', scale.__annotations__)
def strict_total(a, b):
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError('Ожидались целые')
return a + b
print(strict_total(2, 3))
try:
strict_total(2, '3')
except TypeError as e:
print('Отклонено:', e)
class Box:
def __init__(self):
self.total = 0
def hoisted(box, steps):
t = box.total
for i in range(steps):
t += i
box.total = t
b = Box()
hoisted(b, 100)
print(b.total)
1. Что такое Cython?
2. Что делает cdef double s = 0.0?
3. Что делают аннотации типов в обычном Python без Cython?
4. Чем Cython отличается от Nuitka по масштабу?
5. Какой код Cython НЕ ускорит?
6. С чего правильно начинать типизацию?
7. Как программа использует скомпилированный Cython-модуль?
Сделай типизированную имитацию суммы: функция total_typed(values) со локальной переменной s = 0.0 и циклом по values. Посчитай сумму массива array('d') из четырёх чисел и сверь с sum(). Ожидаемый вывод: сумма и True.
Что такое Cython простыми словами?
Надмножество Python: тот же код плюс объявления типов вроде cdef double. Модуль компилируется в C-расширение, которое импортируется обычным import, а типизированные циклы в нём идут без создания объектов — за счёт этого и ускорение.
Урок 12: Nuitka — компиляция всей программыЧем cdef отличается от обычных аннотаций?
Аннотации — подписи для инструментов: Python их хранит и не проверяет. cdef — прямое указание компилятору: переменная становится машинным числом, и код вокруг неё собирается без объектов. Ускорение даёт именно cdef и компиляция, а не аннотации сами по себе.
Ускорит ли Cython мою программу?
Сначала профиль. Вычислительные циклы с большим числом итераций — да, иногда в разы. Код, ждущий сеть или диск, — нет: узкое место не интерпретация. Компиляция функции с долей процента времени общую картину не изменит.
Что такое mypyc и как он связан с Cython?
Родственный компилятор: он тоже превращает Python-код в C-расширение, но использует обычные аннотации типов вместо cdef. На mypyc собран сам статический анализатор mypy — известный пример ускорения через типы.
Как Cython-модуль попадает в exe-сборку?
Как бинарник: PyInstaller подхватывает .pyd-расширение как библиотеку и кладёт в бандл. Исходники .pyx и C-файл в сборку не нужны — пользователь получает только скомпилированное расширение.
Урок 10: PyInstaller — сборка exeПонравился урок? Сошлитесь на него
«Ускорение даёт не наличие типов в коде, а компиляция с ними: до сборки аннотации остаются данными, которые Python не проверяет.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Компиляция · Урок 12
Nuitka: компиляция Python-кода в C
От упаковки к компиляции: как Nuitka переводит программу в C, что становится машинным кодом, а что остаётся PyObject — и где ждать ускорения.
Компиляция · Урок 10
PyInstaller: сборка exe из Python-скрипта
От скрипта к exe: PyInstaller анализирует импорты, кладёт интерпретатор, байткод и данные в один файл. Собираем бандл по шагам и проверяем его на чистой машине.
Компиляция · Урок 14
py2exe и cx_Freeze: альтернативы сборки exe
Сборка exe без PyInstaller: py2exe — старейший упаковщик Windows, cx_Freeze — кросс-платформенный. Общий принцип, recipe на setup.py и правила выбора инструмента.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Функции · Урок 17
Аннотации типов в Python: подсказки для людей
Подписываем аргументы и результат: что дают аннотации, что они не проверяют, и как выглядят типы для списков, словарей и Optional.
python аннотации типованнотации типов python
json · Урок 18
CSV и JSON: две таблицы, два формата
У таблиц два языка: CSV для Excel и плоских выгрузок, JSON для типов и вложенности. Модуль csv переводит с обоих.
csv модуль pythonприведение типов csv python
Компиляция · Урок 17
PyPy: JIT-компилятор Python и когда он выигрывает
Тот же язык, другой движок: PyPy компилирует горячие участки прямо во время работы. Разбираем разогрев, ускорение в разы, совместимость с C-расширениями и случаи, когда CPython остаётся правильным выбором.
ускорение python pypy