コンテンツにスキップ

yieldを持つ依存関係

FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。

これを行うには、returnの代わりにyieldを使い、その後に追加のステップを書きます。

豆知識

yieldは必ず一度だけ使用するようにしてください。

情報

これを動作させるには、Python 3.7 以上を使用するか、Python 3.6 では"backports"をインストールする必要があります:

pip install async-exit-stack async-generator

これによりasync-exit-stackasync-generatorがインストールされます。

技術詳細

以下と一緒に使用できる関数なら何でも有効です:

これらは FastAPI の依存関係として使用するのに有効です。

実際、FastAPIは内部的にこれら2つのデコレータを使用しています。

yieldを持つデータベースの依存関係

例えば、これを使ってデータベースセッションを作成し、終了後にそれを閉じることができます。

レスポンスを送信する前にyield文を含む前のコードのみが実行されます。

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

生成された値は、path operationsや他の依存関係に注入されるものです:

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 は、通常の依存関係と同じように、それぞれで正しいことを行います。

yieldtryを持つ依存関係

yieldを持つ依存関係でtryブロックを使用した場合、その依存関係を使用した際に発生した例外を受け取ることになります。

例えば、途中のどこかの時点で、別の依存関係やpath operationの中で、データベーストランザクションを「ロールバック」したり、その他のエラーを作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。

そのため、依存関係の中にある特定の例外をexcept SomeExceptionで探すことができます。

同様に、finallyを用いて例外があったかどうかにかかわらず、終了ステップを確実に実行することができます。

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

yieldを持つサブ依存関係

任意の大きさや形のサブ依存関係やサブ依存関係の「ツリー」を持つことができ、その中でyieldを使用することができます。

FastAPI は、yieldを持つ各依存関係の「終了コード」が正しい順番で実行されていることを確認します。

例えば、dependency_cdependency_bdependency_bに依存するdependency_aに、依存することができます:

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)
🤓 Other versions and variants
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)

そして、それらはすべてyieldを使用することができます。

この場合、dependency_cは終了コードを実行するために、dependency_b(ここではdep_bという名前)の値がまだ利用可能である必要があります。

そして、dependency_bdependency_a(ここではdep_aという名前)の値を終了コードで利用できるようにする必要があります。

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)
🤓 Other versions and variants
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)

同様に、yieldreturnが混在した依存関係を持つこともできます。

また、単一の依存関係を持っていて、yieldなどの他の依存関係をいくつか必要とすることもできます。

依存関係の組み合わせは自由です。

FastAPI は、全てが正しい順序で実行されていることを確認します。

技術詳細

これはPythonのContext Managersのおかげで動作します。

FastAPI はこれを実現するために内部的に使用しています。

yieldHTTPExceptionを持つ依存関係

yieldと例外をキャッチするtryブロックを持つことができる依存関係を使用することができることがわかりました。

yieldの後の終了コードでHTTPExceptionなどを発生させたくなるかもしれません。しかしそれはうまくいきません

yieldを持つ依存関係の終了コードは例外ハンドラ後に実行されます。依存関係によって投げられた例外を終了コード(yieldの後)でキャッチするものはなにもありません。

つまり、yieldの後にHTTPExceptionを発生させた場合、HTTTPExceptionをキャッチしてHTTP 400のレスポンスを返すデフォルトの(あるいは任意のカスタムの)例外ハンドラは、その例外をキャッチすることができなくなります。

これは、依存関係に設定されているもの(例えば、DBセッション)を、例えば、バックグラウンドタスクで使用できるようにするものです。

バックグラウンドタスクはレスポンスが送信されたに実行されます。そのため、すでに送信されているレスポンスを変更する方法すらないので、HTTPExceptionを発生させる方法はありません。

しかし、バックグラウンドタスクがDBエラーを発生させた場合、少なくともyieldで依存関係のセッションをロールバックしたり、きれいに閉じたりすることができ、エラーをログに記録したり、リモートのトラッキングシステムに報告したりすることができます。

例外が発生する可能性があるコードがある場合は、最も普通の「Python流」なことをして、コードのその部分にtryブロックを追加してください。

レスポンスを返したり、レスポンスを変更したり、HTTPExceptionを発生させたりする前に処理したいカスタム例外がある場合は、カスタム例外ハンドラを作成してください。

豆知識

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 -->> handler: Raise HTTPException
        handler -->> client: HTTP error response
        operation -->> dep: Raise other 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

情報

1つのレスポンス だけがクライアントに送信されます。それはエラーレスポンスの一つかもしれませんし、path operationからのレスポンスかもしれません。

いずれかのレスポンスが送信された後、他のレスポンスを送信することはできません。

豆知識

この図はHTTPExceptionを示していますが、カスタム例外ハンドラを作成することで、他の例外を発生させることもできます。そして、その例外は依存関係の終了コードではなく、そのカスタム例外ハンドラによって処理されます。

しかし例外ハンドラで処理されない例外を発生させた場合は、依存関係の終了コードで処理されます。

コンテキストマネージャ

「コンテキストマネージャ」とは

「コンテキストマネージャ」とは、with文の中で使用できるPythonオブジェクトのことです。

例えば、ファイルを読み込むにはwithを使用することができます:

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

その後のopen("./somefile.txt")は「コンテキストマネージャ」と呼ばれるオブジェクトを作成します。

withブロックが終了すると、例外があったとしてもファイルを確かに閉じます。

yieldを依存関係を作成すると、FastAPI は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。

yieldを持つ依存関係でのコンテキストマネージャの使用

注意

これは多かれ少なかれ、「高度な」発想です。

FastAPI を使い始めたばかりの方は、とりあえずスキップした方がよいかもしれません。

Pythonでは、以下の2つのメソッドを持つクラスを作成する: __enter__()__exit__()ことでコンテキストマネージャを作成することができます。

また、依存関数の中でwithasync with文を使用することによってyieldを持つ FastAPI の依存関係の中でそれらを使用することができます:

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

豆知識

コンテキストマネージャを作成するもう一つの方法はwithです:

これらを使って、関数を単一のyieldでデコレートすることができます。

これは FastAPI が内部的にyieldを持つ依存関係のために使用しているものです。

しかし、FastAPIの依存関係にデコレータを使う必要はありません(そして使うべきではありません)。

FastAPIが内部的にやってくれます。