Ядро без зависимостей: зачем инфраструктурным библиотекам необязательные зависимости¶
Инфраструктурная библиотека попадает во многие сервисы вместе со всеми своими зависимостями. Обычно об этом рассуждают отвлечённо. Я измерил восемь библиотек: что устанавливает базовый пакет, сколько занимает на диске и сколько длится импорт. Самое маленькое ядро устанавливает один дистрибутив и импортируется за 1,2 мс. Вариант с обязательным драйвером БД устанавливает девять и требует около сотни миллисекунд.
Числа получены в эксперименте статьи: временное окружение на пакет, подсчёт установленных файлов и пять измерений импорта. Python 3.13, опубликованные на день измерения версии.
Цена каждой библиотеки¶
package deps MB import ms with the extra
deadline-budget 1 0.1 1.2
clientwright 1 0.3 23.9
grpc-client-kit 3 38.3 27.6 [deadline]: +1 deps, +0.7 MB
redis-client-kit 2 2.6 43.4 [settings]: +7 deps, +8.7 MB
servicewright 1 0.4 30.1 [fastapi]: +22 deps, +16.4 MB
sqlalchemy-foundation-kit 9 18.1 124.7 [metrics]: +1 deps, +7.9 MB
aiokafka-foundation-kit 5 2.0 33.4 [models]: +4 deps, +7.1 MB
pg-partsmith 10 17.4 107.6 [cli]: +9 deps, +14.1 MB
Три библиотеки устанавливают только себя. deadline-budget считает передаваемый бюджет и не требует зависимостей. clientwright создаёт HTTP-клиенты без обязательной HTTP-библиотеки: её выбираете вы. servicewright управляет жизненным циклом без обязательного веб-фреймворка.
Показателен [fastapi]: двадцать два дистрибутива и шестнадцать мегабайт при ядре из одного дистрибутива и четырёхсот килобайт. Сервис с gRPC без HTTP за это не платит.
Какую проблему это предотвращает¶
Главное — разрешение зависимостей, а не диск.
Обязательная FastAPI задаёт диапазон её версий, который задаёт диапазоны Starlette и Pydantic. Другая библиотека сервиса может потребовать несовместимый диапазон, о котором первый автор не знал. Чем больше общих библиотек и сервисов, тем выше вероятность несовместимой пары и необходимости обновлять несвязанное по всей системе.
Каждая обязательная зависимость ограничивает все использующие библиотеку сервисы. Необязательная — только выбравшие интеграцию.
Время импорта тоже имеет значение: 1,2 против 124,7 мс — два порядка, оплачиваемые при запуске процесса, CI, serverless cold start и --help.
Хорошее ядро¶
Три строки с одним дистрибутивом даёт принцип: ядро содержит решения, extras — интеграции.
Самый ясный пример — clientwright. Ядро знает таймауты, повторы, бюджеты, circuit breaker и метрики. Оно не импортирует httpx, aiohttp, requests или urllib3; адаптеры доступны через extras и выбираются по имени при сборке:
Все пять зарегистрированы без установленных HTTP-библиотек: реестр хранит имена и пути импорта, а не импортированные модули. Возможности можно сравнить без установки, отсутствие адаптера становится ошибкой только при его запросе:
Ядро не импортирует фреймворки и необязательные адаптеры. Extras добавляют интеграции вокруг его контрактов; пакет, посвящённый интеграции с SQLAlchemy, при этом может напрямую зависеть от SQLAlchemy.
Сообщение об ошибке — часть возможности¶
У необязательной зависимости есть сценарий отсутствия, и его интерфейс — сообщение. В эксперименте три формы, различающие двухминутную и двадцатиминутную диагностику:
servicewright ImportError: FastAPI support requires servicewright[fastapi]; install it.
redis-client-kit ImportError: pydantic-settings not installed. Install
redis-client-kit[settings] to use BaseRedisSettings.
grpc-client-kit imported; HAS_DEADLINE_BUDGET = False
Первые две называют extra и точно говорят, что установить. Голое ModuleNotFoundError: No module named 'starlette', бывшее до исправления, называет не запрошенный пользователем пакет без очевидной связи с extra.
Третья — намеренная деградация вместо отказа. При отсутствии пакета дедлайнов соответствующий слой не действует, но предупреждает и предоставляет проверяемый флаг: клиент может работать без передачи дедлайна. Это допустимо лишь при безопасной деградации. Аналогичное молчаливое отключение проверки безопасности было бы катастрофой.
Моё правило: отказывать, если отсутствие нарушает корректность; ограничивать возможности, если это допустимо; в обоих случаях сообщать явно.
Когда обязательная зависимость правильна¶
В таблице есть библиотеки с настоящими обязательными зависимостями, и это оправданно.
sqlalchemy-foundation-kit требует SQLAlchemy и asyncpg: девять дистрибутивов, 18 МБ, около 124 мс. Библиотека посвящена async SQLAlchemy с PostgreSQL; делать драйвер необязательным без реального сценария — искусственно. Для pg-partsmith похожую роль играют SQLAlchemy и Pydantic.
Вопрос не «можно ли сделать optional?», а «есть ли реальный пользователь без этой зависимости?». Клиент Redis нужен с redis-py. Жизненный цикл сервиса нужен многим без FastAPI.
Когда одним нужно, другим нет, подходят extra и протокол: ядро структурно определяет контракт, extra даёт реализацию. Так устроены метрики: ядро принимает нужные методы, [metrics] добавляет Prometheus тем, кто его выбрал.
Проверочный список¶
- Можно ли использовать библиотеку без X? Если да, рассмотрите extra.
- Нет ли импорта X на уровне модуля ядра? Один случайный импорт делает optional фактически обязательной. Проверяйте импорты необязательных модулей в окружении без интеграции.
- Называет ли ошибка нужный extra? Проверяйте сообщение, не только тип исключения.
- Безопасен ли запасной путь? Если нет, отказывайте явно.
- Готовы ли вы установить эту зависимость во все свои сервисы? Именно это означает обязательное объявление.
Инструменты¶
Измерены библиотеки Bedrock Python: минимально необходимое ядро, интеграции за extras, структурные протоколы там, где достаточно контракта. Два сообщения исправлены в день написания: статья заставила их измерить.
Один дистрибутив и 1,2 мс либо десятки дистрибутивов и шестнадцать мегабайт. Разницу определяет, кто выбирает интеграцию.