Logo Craft Homelab Docs Контакты Telegram
Изолированное окружение для одной Python-функции: переиспользование кода без конфликта зависимостей Трендовые github проекты в нашем телеграм канале. Подпишись →
6 сентября 2026 г.

Когда общая функция тянет за собой свой список пакетов

Внутренняя утилитная функция редко живёт без зависимостей. Сегодня ей нужен python-dateutil, завтра — конкретная версия NumPy, послезавтра — парсер документов с системными библиотеками. Пока функция используется в одном проекте, это терпимо. Как только тот же код понадобился во втором сервисе, появляется выбор из трёх неудобных вариантов: править lockfile второго проекта, собирать под функцию отдельный контейнер или разворачивать вокруг десяти строк полноценный сервис с HTTP.

Есть промежуточный подход: опубликовать доверенную функцию вместе с точными версиями пакетов и вызывать её по имени. Локальный демон сам собирает нужное окружение и держит его отдельно от проекта, который делает вызов. Ниже разбор того, как это устроено на примере splime — библиотеки для macOS и Linux с Python 3.13+.

Что ломается при переиспользовании

Общий Python-пакет с утилитами — привычное решение, но у него есть цена. Все проекты, которые его подключают, получают объединение зависимостей: если одной функции нужен тяжёлый ML-стек, он приезжает всем. Версии приходится держать совместимыми сразу во всех потребителях, а конфликт двух транзитивных зависимостей блокирует обновление у всех участников одновременно.

Отдельное окружение под функцию снимает эту связанность: проект-потребитель видит только сам вызов, а её пакеты стоят в стороне и не участвуют в разрешении зависимостей проекта.

Функция публикуется вместе с версиями пакетов

Модель работы такая. Запускается локальный демон, который ведёт реестр опубликованных функций, историю запусков и собранные окружения. Клиент регистрирует функцию с описанием входов, выходов, тела и списка дистрибутивов с точными версиями. Демон по этому описанию собирает venv и кэширует его.

Минимальный сценарий: чистое окружение, в котором стоит только splime, и функция, которой нужен отсутствующий в этом окружении python-dateutil.

python3.13 -m venv .venv
. .venv/bin/activate
python -m pip install "splime==0.4.9"
export SPL_DAEMON_HOME="$PWD/.splime-daemon"
spl-daemon serve --auto-port

Демон остаётся работать, а в соседнем терминале выполняется код с вызовом:

import importlib.util
from spl import SPLClient

assert importlib.util.find_spec("dateutil") is None

INVOICE_YAML = """\
- !DFunction
  name: days_until_due
  inputs:
    - {name: issued_at, type: str, default: null}
    - {name: due_at, type: str, default: null}
  outputs:
    - {name: default, type: int}
  body: |-
    from dateutil.parser import isoparse
    return (isoparse(due_at) - isoparse(issued_at)).days
- !DDistribution
  package: python-dateutil
  version: 2.9.0.post0
"""

client = SPLClient()
client.register_env()
client.publish_yaml(INVOICE_YAML, name="days_until_due", entrypoint="days_until_due")

result = client.call(
    "days_until_due",
    kwargs={"issued_at": "2026-09-01", "due_at": "2026-09-30"},
    timeout_seconds=120,
)
print(result.output)  # 29

Первый вызов несколько секунд печатает прогресс сборки, затем выводит 29. Функция получила ровно ту версию пакета, которую объявила, а окружение вызывающего скрипта осталось нетронутым: importlib.util.find_spec("dateutil") в нём по-прежнему возвращает None. Следующий вызов берёт уже готовый venv.

Три места, которые легко перепутать

В этой схеме одновременно живут три разных каталога:

  1. .venv вызывающего скрипта. Здесь стоит splime, но нет python-dateutil.
  2. Каталог демона (daemon home) — реестр функций, запусков и собранных окружений.
  3. venv функции внутри каталога демона. Именно сюда установлен python-dateutil==2.9.0.post0.

Демон не копирует текущее окружение целиком. Он собирает новое из точного списка !DDistribution, поэтому состав пакетов функции предсказуем и воспроизводится на другой машине.

Как определяется список зависимостей

Если публиковать живую функцию через client.publish(), splime разбирает байткод, находит внешние имена, сопоставляет модуль с установленным дистрибутивом через importlib.metadata.packages_distributions() и записывает его версию. Стандартная библиотека отбрасывается.

У механизма есть граница: импорт внутри тела функции не попадает в список глобальных имён. В примере выше from dateutil.parser import isoparse стоит прямо в теле, поэтому зависимость объявлена руками. Если сторонний модуль импортирован на уровне модуля, splime добавит пакет и его версию сам. Когда глобальный модуль не удаётся связать с установленным дистрибутивом, публикация завершается ошибкой и не создаёт заведомо неполный объект.

Кэш окружений по хешу спецификации

Демон строит спецификацию из выбранного интерпретатора, версии Python, списка дистрибутивов, рантайм-пакетов и способа сборки, после чего считает от неё spec_hash. Одинаковая спецификация получает тот же каталог и готовый venv. Меняется версия любого пакета — появляется другой хеш и новое окружение, старое остаётся на месте. Две функции с совпадающим набором зависимостей делят один кэш; несовместимые наборы не смешиваются.

Файл build.lock защищает кэш, если собрать одно окружение одновременно пытаются несколько процессов. По умолчанию сборка стартует в фоне сразу после регистрации, поэтому немедленный первый call() дождётся уже начатой сборки, а не запустит вторую.

Если в PATH есть uv, окружение собирается командой uv venv --relocatable, а пакеты ставятся через uv pip install --strict. Без uv используются штатные venv и pip. Выбранный сборщик входит в хеш спецификации: структура каталогов у этих окружений отличается, и делить кэш между ними нельзя.

Три рантайма для запуска функции

Окружение отвечает на вопрос, какие пакеты и какой Python нужны функции. Способ запуска задаётся отдельно — рантаймом ноды:

  • native — функция выполняется в том же процессе, где вызван client.call(). Режим по умолчанию, минимальные накладные расходы, подходит для доверенного кода.
  • venv-subprocess — запускается отдельный процесс с подготовленным Python и раннером без splime. Нужен, когда важны изоляция процесса и зависимостей.
  • docker — под функцию поднимается контейнер. Нужен для системных пакетов или более жёсткой границы исполнения.

Рантайм можно назначить конкретной ноде пайплайна, не переводя в него весь граф, и переопределить для одного запуска через аргумент runtimes={...} без изменения пайплайна. В версии 0.4.9 у venv-subprocess и per-node Docker есть ограничение: входы должны быть JSON-совместимыми, файловый транспорт пользовательских типов к этим рантаймам ещё не подключён. Для передачи numpy.ndarray между нодами понадобится native или явная нода-конвертер.

Безопасность: venv не даёт песочницы

native и venv-subprocess работают от имени того же пользователя ОС, что и вызывающий процесс. Второй режим отделяет процесс и зависимости, но не запрещает читать файлы, доступные этому пользователю. Виртуальное окружение защищает проекты от конфликта пакетов; машину от кода функции оно не защищает. Для недоверенного кода нужен Docker с проверенными mount-ами либо отдельная учётная запись ОС. В режимах network="auto" и network="none" контейнер стартует с --network none, сеть включается только явным network="enabled".

Когда подход оправдан

Для десятка лёгких функций с одинаковыми зависимостями обычный Python-пакет проще и быстрее. Долгоживущий API для разных языков и клиентов честнее оформить сервисом. Расписания и оркестрацию закрывают Airflow, Prefect и Temporal, распределённые вычисления — Ray.

Отдельное окружение окупается, когда функция уже имеет собственный вес: модель, парсер документов, обработчик табличных данных, код с редкой или конфликтующей зависимостью. Тогда сборка venv возвращается через переиспользование и кэш, а проекты-потребители перестают тянуть на себя чужой стек.

Диагностика сборки

Первый шаг при проблемах со сборкой — команда spl-daemon doctor. Она проверяет Python, venv/ensurepip, наличие uv, daemon home, свободное место, доступность демона, кэш окружений, подмены интерпретатора и Docker. Историю сборок показывает spl-daemon env-build-list, конкретную — spl-daemon env-build-show <spec-hash>. Полный вывод установщика лежит в install.log: если статус стал failed, причина обычно уже там, например пакет не существует для выбранной версии Python.