Зависимости с yield и управление ресурсами

Зависимость с yield отдаёт ресурс до yield и гарантированно прибирает его после — код после yield выполняется, когда запрос завершён.

Это паттерн «открыть — отдать — закрыть»: до yield вы создаёте сессию БД, отдаёте её обработчику через yield, а после — закрываете, даже если внутри произошла ошибка.

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

from fastapi import Depends
from typing import Annotated

def get_db():
    db = SessionLocal()          # открываем сессию
    try:
        yield db                 # отдаём её обработчику
    finally:
        db.close()               # закрываем ВСЕГДА, даже при ошибке

DBSession = Annotated[Session, Depends(get_db)]

@app.get("/items")
async def list_items(db: DBSession):
    return db.query(Item).all()

Ключевой момент — try/finally: блок finally выполнится в любом случае, поэтому сессия закроется и при успехе, и при исключении в обработчике. По сути зависимость с yield ведёт себя как контекстный менеджер: вход — до yield, выход — после.

Как работает под капотом

FastAPI продвигает генератор до yield, забирает отданное значение, выполняет обработчик, а затем «дотягивает» генератор до конца, исполняя уборку. Смоделируем этот жизненный цикл на stdlib:

def get_db():
    print("1) открываем сессию БД")
    db = {"connected": True}
    try:
        yield db
    finally:
        db["connected"] = False
        print("4) закрываем сессию БД (finally)")

# то, как FastAPI «прокручивает» зависимость вокруг обработчика
gen = get_db()
db = next(gen)                 # доходим до yield -> получаем ресурс
print("2) обработчик использует сессию, connected =", db["connected"])
print("3) обработчик вернул результат")
try:
    next(gen)                  # дотягиваем генератор -> срабатывает finally
except StopIteration:
    pass
print("после запроса connected =", db["connected"])

Попробуй сам ▶ Порядок 1-2-3-4 показывает: уборка (4) гарантированно следует за работой обработчика. Попробуй кинуть исключение между шагами — finally всё равно сработает.

Порядок при нескольких зависимостях с yield — стековый: уборка идёт в обратном порядке создания (как в матрёшке контекстных менеджеров).

Частые ошибки

Первая и критичная — забыть try/finally и закрывать ресурс только при успехе; при исключении он утечёт. Вторая — закрывать сессию вручную внутри обработчика, дублируя логику и ломая контракт зависимости. Третья — выполнять тяжёлую работу после yield (уборка должна быть быстрой). Четвёртая — путать, что код после yield выполняется не сразу, а по завершении запроса.

Best practices

  • Всегда оборачивайте уборку в try/finally, чтобы она срабатывала и при ошибках.
  • Открывайте ресурс до yield, отдавайте через yield, закрывайте после.
  • Держите код после yield лёгким — это уборка, а не бизнес-логика.
  • Для async-ресурсов используйте async def зависимость с yield и await-закрытием.

Async-зависимости и порядок уборки

Для асинхронных ресурсов зависимость с yield объявляют как async def и закрывают ресурс через await — например, асинхронную сессию SQLAlchemy. Важно понимать порядок уборки при нескольких зависимостях с yield: он стековый, обратный порядку создания, как у вложенных контекстных менеджеров. Если зависимость A открылась первой, а B — второй, то при завершении запроса сначала уберётся B, потом A. Это гарантирует корректность, когда B зависит от A (например, транзакция внутри соединения): нельзя закрыть соединение раньше, чем завершить транзакцию. FastAPI соблюдает этот порядок автоматически. Ещё нюанс: если исключение возникло в обработчике, оно «прокидывается» в зависимости с yield, и вы можете перехватить его в блоке вокруг yield, чтобы, скажем, откатить транзакцию — именно так и строят транзакционные сессии.

Итог: зависимость с yield — это контекстный менеджер для ресурсов: подготовка до yield, гарантированная уборка после, защищённая try/finally. Это стандартный способ управлять сессиями БД в FastAPI.

Проверьте себя
1. Почему уборку ресурса в зависимости с yield помещают в блок finally?
AДля красоты кода
BЧтобы ресурс закрывался в любом случае — и при успехе, и при исключении в обработчике
CЧтобы ускорить запрос
Dfinally здесь не нужен
2. Когда выполняется код, расположенный после yield в зависимости?
AСразу после открытия ресурса
BНикогда
CПосле завершения обработки запроса — как уборка
DПеред вызовом обработчика