Прямой ответ
Бинарное расширение — модуль, скомпилированный в машинный код и загружаемый интерпретатором. Оно нужно в двух случаях: ускорение вычислительно нагруженного кода и доступ к системной библиотеке, не имеющей готовой обёртки.
Перед компиляцией стоит проверить альтернативы. Ограничение глобальной блокировкой интерпретатора не снимается расширением для кода, ожидающего ввод-вывод; о природе ограничения — в статье про многопроцессность и GIL. Для вычислительных задач сначала проверяются специализированные библиотеки, и только при их недостаточности пишется расширение.
Сборка описывается в конфигурации пакета:
# pyproject.toml
[build-system]
requires = ["setuptools>=68", "Cython>=3.0"]
build-backend = "setuptools.build_meta"
# setup.py — объявление расширения
from setuptools import setup, Extension
setup(
ext_modules=[
Extension(
"my_package.fastpath",
sources=["src/my_package/fastpath.pyx"],
)
]
)
Файл .pyx компилируется в C, затем в разделяемую библиотеку. Результат — файл с расширением .so в Linux и .pyd в Windows.
Ключевое следствие компиляции — привязка к версии интерпретатора. Расширение собирается под конкретную версию и ABI, поэтому wheel получает платформенный тег:
my_package-1.0.0-cp312-cp312-manylinux_2_17_x86_64.whl
Пользователь с Python 3.11 не сможет установить этот wheel, и при отсутствии дистрибутива под его версию установка пойдёт из исходников с компиляцией; о форматах — в статье про wheel и sdist.
Поэтому публикация бинарного пакета требует сборки под набор платформ и версий:
cibuildwheel --platform linux
Инструмент собирает wheel в контейнерах для каждой поддерживаемой версии Python, что обеспечивает совместимость с широким набором дистрибутивов.
Стоимость поддержки бинарного расширения
Решение о компиляции принимается один раз, а издержки возникают при каждом релизе.
Пересборка под каждую версию Python. Новая версия интерпретатора требует нового набора дистрибутивов. До публикации wheel под новую версию пользователи устанавливают пакет из исходников либо не могут обновиться.
Набор платформ. Поддержка Linux, macOS и Windows умножает число дистрибутивов на число версий Python и архитектур. Для трёх платформ, двух архитектур и трёх версий Python это восемнадцать дистрибутивов на одну версию пакета.
Отладка. Ошибка в скомпилированном коде проявляется как аварийное завершение интерпретатора, а не как исключение Python. Разбор требует инструментов отладки C и воспроизведения на конкретной платформе.
Зависимость от системных библиотек. Расширение, связанное с системной библиотекой, наследует её ограничения по версии. Сборка на свежем дистрибутиве создаёт дистрибутив, не работающий в системах с более старой библиотекой.
Проверка безопасности. Код на C требует отдельного анализа на ошибки работы с памятью, которые в Python невозможны.
Перед решением о компиляции стоит оценить, покрывается ли задача готовой библиотекой с уже собранными дистрибутивами. Стоимость поддержки собственного расширения часто превышает выигрыш от ускорения.
Практика: сборка и публикация расширения
1. Проверяйте необходимость компиляции. Профилирование показывает, какая часть кода требует ускорения. Компиляция всего модуля без измерения даёт сложность без выигрыша.
2. Собирайте wheel в конвейере для набора платформ. Сборка на машине разработчика даёт дистрибутив только под его платформу; о настройке — в статье про конвейер CI.
3. Используйте контейнеры для сборки под Linux. Сборка на свежем дистрибутиве создаёт wheel, не работающий в системах с более старой системной библиотекой.
4. Тестируйте на каждой целевой платформе. Тесты выполняются для каждого собранного wheel, а не только на платформе сборки.
5. Публикуйте и sdist. Пользователь на платформе без готового wheel соберёт пакет из исходников при наличии компилятора.
6. Фиксируйте версии инструментов сборки. Компилятор и версия Cython влияют на результат, поэтому их версии фиксируются в требованиях сборки.
7. Проверяйте размер дистрибутива. Набор wheel под несколько платформ и версий увеличивает объём публикации в десятки раз.
8. Документируйте требования к сборке из исходников. Пользователю нужны компилятор и заголовочные файлы, и их отсутствие должно давать понятное сообщение.
Рекомендации:
- Начинайте с минимального расширения. Компиляция узкого участка кода проще отладки большого скомпилированного модуля.
- Сохраняйте чистый интерфейс на Python. Вызывающий код не должен знать, реализована функция на Python или в расширении.
- Проверяйте обработку ошибок в расширении. Исключение, возникшее в скомпилированном коде, должно корректно передаваться в Python.
- Учитывайте стоимость поддержки. Каждая новая версия Python требует пересборки и проверки расширения.
- Измеряйте выигрыш после компиляции. Ускорение может оказаться меньше ожидаемого из-за накладных расходов на передачу данных.
Практический сценарий: недоступность пакета у части пользователей из-за отсутствия платформенных wheel
Контекст: пакет с бинарным расширением публиковался сборкой на машине разработчика. Пользователи с другой версией Python получали ошибку компиляции при установке.
- Диагностика. В реестре был единственный wheel с тегом
cp312под Linux x86_64. Пользователи с Python 3.11 и с другой архитектурой устанавливали пакет из sdist, что требовало компилятор и заголовочные файлы. - Причина. Сборка выполнялась вручную в локальном окружении, поэтому дистрибутив создавался только под текущую платформу. Проверка установки на других версиях не выполнялась.
- Действия. (а) Сборку перенесли в конвейер с использованием инструмента, создающего wheel для набора версий Python и платформ; (б) добавили прогон тестов для каждого собранного wheel; (в) опубликовали дистрибутивы под поддерживаемые версии и сохранили sdist для остальных случаев; (г) добавили в документацию требования к сборке из исходников; (д) зафиксировали версии компилятора и Cython в требованиях сборки.
- Результат. Установка перестала требовать компилятора для поддерживаемых сочетаний, а время установки сократилось благодаря отсутствию шага сборки.
Вывод: бинарное расширение привязывает пакет к версии интерпретатора и платформе, поэтому публикация требует сборки набора дистрибутивов в конвейере. Единственный wheel, собранный вручную, оставляет часть пользователей со сборкой из исходников и зависимостью от компилятора.
FAQ
Когда нужно бинарное расширение? Для вычислительно нагруженного кода после профилирования и для доступа к системной библиотеке без готовой обёртки. Для кода, ожидающего ввод-вывод, компиляция не даёт выигрыша.
Почему wheel привязан к версии Python? Расширение собирается под конкретный ABI интерпретатора. Тег в имени wheel описывает совместимую версию и платформу.
Что делать пользователям без подходящего wheel? Устанавливать из sdist при наличии компилятора и заголовочных файлов. Отсутствие инструментов сборки делает установку невозможной.
Как снизить число собираемых дистрибутивов? Ограничить поддерживаемые версии Python и платформы, а для остальных случаев публиковать sdist.
Источники: Cython docs, setuptools docs (Extension), cibuildwheel docs — ссылки в шапке.