跳至內容

使用 yield 的依賴項

FastAPI 支援在完成後執行一些額外步驟的依賴項。

要做到這一點,請使用 yield 而不是 return,並在後面撰寫額外的步驟(程式碼)。

提示

確保每個依賴項只使用一次 yield

「技術細節」

任何有效用於

都可以作為 FastAPI 的依賴項。

事實上,FastAPI 在內部使用了這兩個裝飾器。

使用 yield 的資料庫依賴項

例如,您可以使用它來建立資料庫工作階段並在完成後關閉它。

只有在 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 會正確處理每一種情況,與一般依賴項相同。

使用 yieldtry 的依賴項

如果您在使用 yield 的依賴項中使用 try 區塊,您將收到使用該依賴項時拋出的任何例外。

例如,如果在中間的某個地方,在另一個依賴項或路徑操作中,程式碼使資料庫交易「回滾」或產生任何其他錯誤,您將在您的依賴項中收到該例外。

因此,您可以使用 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)

提示

如果可能,請盡量使用 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=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)

提示

如果可能,請盡量使用 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=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 在內部使用它們來實現這一點。

使用 yieldHTTPException 的依賴項

您已經看到您可以使用帶有 yield 的依賴項,並使用 try 區塊來捕捉例外。

同樣地,您可以在 yield 之後的退出代碼中引發 HTTPException 或類似例外。

提示

這是一種比較進階的技巧,在大多數情況下,您並不需要它,因為您可以從應用程式程式碼的其他部分(例如,在*路徑操作函式*中)引發例外(包括 HTTPException)。

但如果您需要它,它就在那裡。🤓

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()


data = {
    "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
    "portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}


class OwnerError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except OwnerError as e:
        raise HTTPException(status_code=400, detail=f"Owner error: {e}")


@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
    if item_id not in data:
        raise HTTPException(status_code=404, detail="Item not found")
    item = data[item_id]
    if item["owner"] != username:
        raise OwnerError(username)
    return item
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated

app = FastAPI()


data = {
    "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
    "portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}


class OwnerError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except OwnerError as e:
        raise HTTPException(status_code=400, detail=f"Owner error: {e}")


@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
    if item_id not in data:
        raise HTTPException(status_code=404, detail="Item not found")
    item = data[item_id]
    if item["owner"] != username:
        raise OwnerError(username)
    return item

提示

如果可能,請盡量使用 Annotated 版本。

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()


data = {
    "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
    "portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}


class OwnerError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except OwnerError as e:
        raise HTTPException(status_code=400, detail=f"Owner error: {e}")


@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
    if item_id not in data:
        raise HTTPException(status_code=404, detail="Item not found")
    item = data[item_id]
    if item["owner"] != username:
        raise OwnerError(username)
    return item

您可以用來捕捉例外(並且可能也引發另一個 HTTPException)的替代方法是建立自訂例外處理器

使用 yieldexcept 的依賴項

如果您在使用 yield 的依賴項中使用 except 捕捉到例外,並且您沒有再次引發它(或引發新的例外),FastAPI 將無法注意到發生了例外,就像在一般 Python 中一樣。

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()


class InternalError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except InternalError:
        print("Oops, we didn't raise again, Britney 😱")


@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
    if item_id == "portal-gun":
        raise InternalError(
            f"The portal gun is too dangerous to be owned by {username}"
        )
    if item_id != "plumbus":
        raise HTTPException(
            status_code=404, detail="Item not found, there's only a plumbus here"
        )
    return item_id
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated

app = FastAPI()


class InternalError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except InternalError:
        print("Oops, we didn't raise again, Britney 😱")


@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
    if item_id == "portal-gun":
        raise InternalError(
            f"The portal gun is too dangerous to be owned by {username}"
        )
    if item_id != "plumbus":
        raise HTTPException(
            status_code=404, detail="Item not found, there's only a plumbus here"
        )
    return item_id

提示

如果可能,請盡量使用 Annotated 版本。

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()


class InternalError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except InternalError:
        print("Oops, we didn't raise again, Britney 😱")


@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
    if item_id == "portal-gun":
        raise InternalError(
            f"The portal gun is too dangerous to be owned by {username}"
        )
    if item_id != "plumbus":
        raise HTTPException(
            status_code=404, detail="Item not found, there's only a plumbus here"
        )
    return item_id

在這種情況下,用戶端會看到*HTTP 500 內部伺服器錯誤*回應,因為我們沒有引發 HTTPException 或類似例外,但伺服器將**沒有任何記錄**或任何其他錯誤指示。😱

始終在使用 yieldexcept 的依賴項中 raise

如果您在使用 yield 的依賴項中捕捉到例外,除非您要引發另一個 HTTPException 或類似例外,否則您應該重新引發原始例外。

您可以使用 raise 重新引發相同的例外。

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()


class InternalError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except InternalError:
        print("We don't swallow the internal error here, we raise again 😎")
        raise


@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
    if item_id == "portal-gun":
        raise InternalError(
            f"The portal gun is too dangerous to be owned by {username}"
        )
    if item_id != "plumbus":
        raise HTTPException(
            status_code=404, detail="Item not found, there's only a plumbus here"
        )
    return item_id
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated

app = FastAPI()


class InternalError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except InternalError:
        print("We don't swallow the internal error here, we raise again 😎")
        raise


@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
    if item_id == "portal-gun":
        raise InternalError(
            f"The portal gun is too dangerous to be owned by {username}"
        )
    if item_id != "plumbus":
        raise HTTPException(
            status_code=404, detail="Item not found, there's only a plumbus here"
        )
    return item_id

提示

如果可能,請盡量使用 Annotated 版本。

from fastapi import Depends, FastAPI, HTTPException

app = FastAPI()


class InternalError(Exception):
    pass


def get_username():
    try:
        yield "Rick"
    except InternalError:
        print("We don't swallow the internal error here, we raise again 😎")
        raise


@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
    if item_id == "portal-gun":
        raise InternalError(
            f"The portal gun is too dangerous to be owned by {username}"
        )
    if item_id != "plumbus":
        raise HTTPException(
            status_code=404, detail="Item not found, there's only a plumbus here"
        )
    return item_id

現在用戶端將收到相同的*HTTP 500 內部伺服器錯誤*回應,但伺服器記錄中將會有我們自訂的 InternalError。😎

使用 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,operation: Can raise exceptions, including HTTPException
    client ->> dep: Start request
    Note over dep: Run code up to yield
    opt raise Exception
        dep -->> handler: Raise Exception
        handler -->> client: HTTP error response
    end
    dep ->> operation: Run dependency, e.g. DB session
    opt raise
        operation -->> dep: Raise Exception (e.g. HTTPException)
        opt handle
            dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception
        end
        handler -->> client: HTTP error response
    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 -->> tasks: Handle exceptions in the background task code
    end

資訊

只會向用戶端發送**一個回應**。它可能是其中一個錯誤回應,也可能是來自*路徑操作*的回應。

發送其中一個回應後,就無法再發送其他回應。

提示

此圖顯示了 HTTPException,但您也可以引發您在使用 yield 的依賴項或自訂例外處理器中捕捉到的任何其他例外。

如果您引發任何例外,它將會透過 `yield` 傳遞給依賴項,包括 `HTTPException`。在大多數情況下,您會希望使用 `yield` 重新引發相同的例外或從依賴項中引發新的例外,以確保它得到正確的處理。

帶有 `yield`、`HTTPException`、`except` 和背景任務的依賴項

警告

您很可能不需要這些技術細節,您可以跳過此章節並繼續下面的內容。

這些細節主要在您使用 0.106.0 之前的 FastAPI 版本,並且在背景任務中使用了來自帶有 `yield` 的依賴項的資源時才有用。

帶有 `yield` 和 `except` 的依賴項,技術細節

在 FastAPI 0.110.0 之前,如果您使用帶有 `yield` 的依賴項,然後在該依賴項中使用 `except` 捕獲了例外,並且您沒有再次引發該例外,則該例外將會自動引發/轉發到任何例外處理程式或內部伺服器錯誤處理程式。

這在 0.110.0 版本中做了更改,以修復因轉發沒有處理程式的例外(內部伺服器錯誤)而導致的未處理記憶體消耗問題,並使其與常規 Python 程式碼的行為一致。

背景任務和帶有 `yield` 的依賴項,技術細節

在 FastAPI 0.106.0 之前,在 `yield` 之後引發例外是不可能的,帶有 `yield` 的依賴項中的退出程式碼是在傳送回應*之後*執行的,因此例外處理程式將已經運行。

這樣設計的主要目的是允許在背景任務中使用由依賴項「yield」出的相同物件,因為退出程式碼將在背景任務完成後執行。

然而,由於這意味著在不必要地持有帶有 yield 的依賴項中的資源(例如資料庫連線)的同時,需要等待回應通過網路傳輸,因此在 FastAPI 0.106.0 中對此進行了更改。

提示

此外,背景任務通常是一組獨立的邏輯,應該使用其自身的資源(例如其自身的資料庫連線)單獨處理。

因此,這樣您可能會擁有更乾淨的程式碼。

如果您以前依賴於這種行為,現在您應該在背景任務本身內部為背景任務建立資源,並且僅在內部使用不依賴於帶有 `yield` 的依賴項的資源的資料。

例如,您將在背景任務內部建立一個新的資料庫連線,而不是使用相同的資料庫連線,並且您將使用這個新的連線從資料庫中獲取物件。然後,您將傳遞該物件的 ID,而不是將來自資料庫的物件作為參數傳遞給背景任務函數,然後在背景任務函數內部再次獲取該物件。

上下文管理器

什麼是「上下文管理器」

「上下文管理器」是指您可以在 `with` 陳述式中使用的任何 Python 物件。

例如,您可以使用 `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 使用它們,方法是在依賴項函式內使用 withasync 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 會在內部為您執行此操作。