yieldを持つ依存関係¶
FastAPIは、いくつかの終了後の追加のステップを行う依存関係をサポートしています。
これを行うには、return
の代わりにyield
を使い、その後に追加のステップを書きます。
豆知識
yield
は必ず一度だけ使用するようにしてください。
情報
これを動作させるには、Python 3.7 以上を使用するか、Python 3.6 では"backports"をインストールする必要があります:
pip install async-exit-stack async-generator
これによりasync-exit-stackとasync-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 は、通常の依存関係と同じように、それぞれで正しいことを行います。
yield
とtry
を持つ依存関係¶
yield
を持つ依存関係でtry
ブロックを使用した場合、その依存関係を使用した際に発生した例外を受け取ることになります。
例えば、途中のどこかの時点で、別の依存関係やpath operationの中で、データベーストランザクションを「ロールバック」したり、その他のエラーを作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。
そのため、依存関係の中にある特定の例外を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 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_b
はdependency_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)
同様に、yield
とreturn
が混在した依存関係を持つこともできます。
また、単一の依存関係を持っていて、yield
などの他の依存関係をいくつか必要とすることもできます。
依存関係の組み合わせは自由です。
FastAPI は、全てが正しい順序で実行されていることを確認します。
yield
とHTTPException
を持つ依存関係¶
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__()
ことでコンテキストマネージャを作成することができます。
また、依存関数の中でwith
やasync 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が内部的にやってくれます。