Перейти к содержанию

Зависимости с yield

FastAPI поддерживает зависимости, которые выполняют некоторые дополнительные действия после завершения работы.

Для этого используйте yield вместо return, а дополнительный код напишите после него.

Подсказка

Обязательно используйте yield один-единственный раз.

Технические детали

Любая функция, с которой может работать:

будет корректно использоваться в качестве FastAPI-зависимости.

На самом деле, FastAPI использует эту пару декораторов "под капотом".

Зависимость базы данных с помощью yield

Например, с его помощью можно создать сессию работы с базой данных и закрыть его после завершения.

Перед созданием ответа будет выполнен только код до и включая yield.

async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

Полученное значение и есть то, что будет внедрено в функцию операции пути и другие зависимости:

async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

Код, следующий за оператором yield, выполняется после доставки ответа:

async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

Подсказка

Можно использовать как async так и обычные функции.

FastAPI это корректно обработает, и в обоих случаях будет делать то же самое, что и с обычными зависимостями.

Зависимость с yield и try одновременно

Если использовать блок try в зависимости с yield, то будет получено всякое исключение, которое было выброшено при использовании зависимости.

Например, если какой-то код в какой-то момент в середине, в другой зависимости или в функции операции пути, сделал "откат" транзакции базы данных или создал любую другую ошибку, то вы получите исключение в своей зависимости.

Таким образом, можно искать конкретное исключение внутри зависимости с помощью except SomeException.

Таким же образом можно использовать finally, чтобы убедиться, что обязательные шаги при выходе выполнены, независимо от того, было ли исключение или нет.

async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

Подзависимости с yield

Вы можете иметь подзависимости и "деревья" подзависимостей любого размера и формы, и любая из них или все они могут использовать yield.

FastAPI будет следить за тем, чтобы "код по выходу" в каждой зависимости с yield выполнялся в правильном порядке.

Например, dependency_c может иметь зависимость от dependency_b, а dependency_b от dependency_a:

from typing import Annotated

from fastapi import Depends


async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()


async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        dep_b.close(dep_a)


async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)
from fastapi import Depends
from typing_extensions import Annotated


async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()


async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        dep_b.close(dep_a)


async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)

Подсказка

Предпочтительнее использовать версию с аннотацией, если это возможно.

from fastapi import Depends


async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()


async def dependency_b(dep_a=Depends(dependency_a)):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        dep_b.close(dep_a)


async def dependency_c(dep_b=Depends(dependency_b)):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)

И все они могут использовать yield.

В этом случае dependency_c для выполнения своего кода выхода нуждается в том, чтобы значение из dependency_b (здесь dep_b) было еще доступно.

И, в свою очередь, dependency_b нуждается в том, чтобы значение из dependency_a (здесь dep_a) было доступно для ее завершающего кода.

from typing import Annotated

from fastapi import Depends


async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()


async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        dep_b.close(dep_a)


async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)
from fastapi import Depends
from typing_extensions import Annotated


async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()


async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        dep_b.close(dep_a)


async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)

Подсказка

Предпочтительнее использовать версию с аннотацией, если это возможно.

from fastapi import Depends


async def dependency_a():
    dep_a = generate_dep_a()
    try:
        yield dep_a
    finally:
        dep_a.close()


async def dependency_b(dep_a=Depends(dependency_a)):
    dep_b = generate_dep_b()
    try:
        yield dep_b
    finally:
        dep_b.close(dep_a)


async def dependency_c(dep_b=Depends(dependency_b)):
    dep_c = generate_dep_c()
    try:
        yield dep_c
    finally:
        dep_c.close(dep_b)

Точно так же можно иметь часть зависимостей с yield, часть с return, и какие-то из них могут зависеть друг от друга.

Либо у вас может быть одна зависимость, которая требует несколько других зависимостей с yield и т.д.

Комбинации зависимостей могут быть какими вам угодно.

FastAPI проследит за тем, чтобы все выполнялось в правильном порядке.

Технические детали

Это работает благодаря Контекстным менеджерам в Python.

FastAPI использует их "под капотом" с этой целью.

Зависимости с yield и HTTPException

Вы видели, что можно использовать зависимости с yield совместно с блоком try, отлавливающие исключения.

Таким же образом вы можете поднять исключение HTTPException или что-то подобное в завершающем коде, после yield.

Код выхода в зависимостях с yield выполняется после отправки ответа, поэтому Обработчик исключений уже будет запущен. В коде выхода (после yield) нет ничего, перехватывающего исключения, брошенные вашими зависимостями.

Таким образом, если после yield возникает HTTPException, то стандартный (или любой пользовательский) обработчик исключений, который перехватывает HTTPException и возвращает ответ HTTP 400, уже не сможет перехватить это исключение.

Благодаря этому все, что установлено в зависимости (например, сеанс работы с БД), может быть использовано, например, фоновыми задачами.

Фоновые задачи выполняются после отправки ответа. Поэтому нет возможности поднять HTTPException, так как нет даже возможности изменить уже отправленный ответ.

Но если фоновая задача создает ошибку в БД, то, по крайней мере, можно сделать откат или чисто закрыть сессию в зависимости с помощью yield, а также, возможно, занести ошибку в журнал или сообщить о ней в удаленную систему отслеживания.

Если у вас есть код, который, как вы знаете, может вызвать исключение, сделайте самую обычную/"питонячью" вещь и добавьте блок try в этот участок кода.

Если у вас есть пользовательские исключения, которые вы хотите обрабатывать до возврата ответа и, возможно, модифицировать ответ, даже вызывая HTTPException, создайте Cобственный обработчик исключений.

Подсказка

Вы все еще можете вызывать исключения, включая HTTPException, до yield. Но не после.

Последовательность выполнения примерно такая, как на этой схеме. Время течет сверху вниз. А каждый столбец - это одна из частей, взаимодействующих с кодом или выполняющих код.

sequenceDiagram

participant client as Client
participant handler as Exception handler
participant dep as Dep with yield
participant operation as Path Operation
participant tasks as Background tasks

    Note over client,tasks: Can raise exception for dependency, handled after response is sent
    Note over client,operation: Can raise HTTPException and can change the response
    client ->> dep: Start request
    Note over dep: Run code up to yield
    opt raise
        dep -->> handler: Raise HTTPException
        handler -->> client: HTTP error response
        dep -->> dep: Raise other exception
    end
    dep ->> operation: Run dependency, e.g. DB session
    opt raise
        operation -->> dep: Raise HTTPException
        dep -->> handler: Auto forward exception
        handler -->> client: HTTP error response
        operation -->> dep: Raise other exception
        dep -->> handler: Auto forward exception
    end
    operation ->> client: Return response to client
    Note over client,operation: Response is already sent, can't change it anymore
    opt Tasks
        operation -->> tasks: Send background tasks
    end
    opt Raise other exception
        tasks -->> dep: Raise other exception
    end
    Note over dep: After yield
    opt Handle other exception
        dep -->> dep: Handle exception, can't change response. E.g. close DB session.
    end

Дополнительная информация

Клиенту будет отправлен только один ответ. Это может быть один из ответов об ошибке или это будет ответ от операции пути.

После отправки одного из этих ответов никакой другой ответ не может быть отправлен.

Подсказка

На этой диаграмме показано "HttpException", но вы также можете вызвать любое другое исключение, для которого вы создаете Пользовательский обработчик исключений.

Если вы создадите какое-либо исключение, оно будет передано зависимостям с yield, включая HttpException, а затем снова обработчикам исключений. Если для этого исключения нет обработчика исключений, то оно будет обработано внутренним "ServerErrorMiddleware" по умолчанию, возвращающим код состояния HTTP 500, чтобы уведомить клиента, что на сервере произошла ошибка.

Зависимости с yield, HTTPException и фоновыми задачами

Внимание

Скорее всего, вам не нужны эти технические подробности, вы можете пропустить этот раздел и продолжить ниже.

Эти подробности полезны, главным образом, если вы использовали версию FastAPI до 0.106.0 и использовали ресурсы из зависимостей с yield в фоновых задачах.

До версии FastAPI 0.106.0 вызывать исключения после yield было невозможно, код выхода в зависимостях с yield выполнялся после отправки ответа, поэтому Обработчик Ошибок уже был бы запущен.

Это было сделано главным образом для того, чтобы позволить использовать те же объекты, "отданные" зависимостями, внутри фоновых задач, поскольку код выхода будет выполняться после завершения фоновых задач.

Тем не менее, поскольку это означало бы ожидание ответа в сети, а также ненужное удержание ресурса в зависимости от доходности (например, соединение с базой данных), это было изменено в FastAPI 0.106.0.

Подсказка

Кроме того, фоновая задача обычно представляет собой независимый набор логики, который должен обрабатываться отдельно, со своими собственными ресурсами (например, собственным подключением к базе данных). Таким образом, вы, вероятно, получите более чистый код.

Если вы полагались на это поведение, то теперь вам следует создавать ресурсы для фоновых задач внутри самой фоновой задачи, а внутри использовать только те данные, которые не зависят от ресурсов зависимостей с yield.

Например, вместо того чтобы использовать ту же сессию базы данных, вы создадите новую сессию базы данных внутри фоновой задачи и будете получать объекты из базы данных с помощью этой новой сессии. А затем, вместо того чтобы передавать объект из базы данных в качестве параметра в функцию фоновой задачи, вы передадите идентификатор этого объекта, а затем снова получите объект в функции фоновой задачи.

Контекстные менеджеры

Что такое "контекстные менеджеры"

"Контекстные менеджеры" - это любые объекты Python, которые можно использовать в операторе with.

Например, можно использовать with для чтения файла:

with open("./somefile.txt") as f:
    contents = f.read()
    print(contents)

Под капотом" open("./somefile.txt") создаёт объект называемый "контекстным менеджером".

Когда блок with завершается, он обязательно закрывает файл, даже если были исключения.

Когда вы создаете зависимость с помощью yield, FastAPI внутренне преобразует ее в контекстный менеджер и объединяет с некоторыми другими связанными инструментами.

Использование менеджеров контекста в зависимостях с помощью yield

Внимание

Это более или менее "продвинутая" идея.

Если вы только начинаете работать с FastAPI, то лучше пока пропустить этот пункт.

В Python для создания менеджеров контекста можно создать класс с двумя методами: __enter__() и __exit__().

Вы также можете использовать их внутри зависимостей FastAPI с yield, используя операторы with или async with внутри функции зависимости:

class MySuperContextManager:
    def __init__(self):
        self.db = DBSession()

    def __enter__(self):
        return self.db

    def __exit__(self, exc_type, exc_value, traceback):
        self.db.close()


async def get_db():
    with MySuperContextManager() as db:
        yield db

Подсказка

Другой способ создания контекстного менеджера - с помощью:

используйте их для оформления функции с одним yield.

Это то, что FastAPI использует внутри себя для зависимостей с yield.

Но использовать декораторы для зависимостей FastAPI не обязательно (да и не стоит).

FastAPI сделает это за вас на внутреннем уровне.