Skip to content

以類別作為相依性

🌐 AI 與人類共同完成的翻譯

此翻譯由人類指導的 AI 完成。🤝

可能會有對原意的誤解,或讀起來不自然等問題。🤖

你可以透過協助我們更好地引導 AI LLM來改進此翻譯。

英文版

在更深入了解 相依性注入(Dependency Injection) 系統之前,我們先把前一個範例升級一下。

前一個範例中的 dict

在前一個範例中,我們從相依項("dependable")回傳了一個 dict

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

但接著我們在路徑操作函式(path operation function)的參數 commons 中取得的是一個 dict

而我們知道,編輯器對 dict 無法提供太多輔助(例如自動完成),因為它無法預先知道其中的鍵與值的型別。

我們可以做得更好...

什麼算是相依性

到目前為止,你看到的相依性都是宣告成函式。

但那不是宣告相依性的唯一方式(雖然那大概是最常見的)。

關鍵在於,相依性應該要是「callable」。

在 Python 中,「callable」指的是任何可以像函式一樣被 Python「呼叫」的東西。

因此,如果你有一個物件 something(它可能不是函式),而你可以像這樣「呼叫」(執行)它:

something()

或是

something(some_argument, some_keyword_argument="foo")

那它就是一個「callable」。

以類別作為相依性

你可能已經注意到,建立一個 Python 類別的實例時,你用的語法也是一樣的。

例如:

class Cat:
    def __init__(self, name: str):
        self.name = name


fluffy = Cat(name="Mr Fluffy")

在這個例子中,fluffyCat 類別的一個實例。

而要建立 fluffy,你其實是在「呼叫」Cat

所以,Python 類別本身也是一種 callable

因此,在 FastAPI 中,你可以將 Python 類別作為相依性。

FastAPI 其實檢查的是它是否為「callable」(函式、類別或其他),以及它所定義的參數。

如果你在 FastAPI 中傳入一個「callable」作為相依性,FastAPI 會分析該「callable」的參數,並以與路徑操作函式參數相同的方式來處理它們,包括子相依性。

這也適用於完全沒有參數的 callable,就和沒有參數的路徑操作函式一樣。

接著,我們可以把上面的相依項(dependable)common_parameters 改成類別 CommonQueryParams

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

注意用來建立該類別實例的 __init__ 方法:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

...它的參數與我們之前的 common_parameters 相同:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

FastAPI 會用這些參數來「解析」該相依性。

兩種情況下都會有:

  • 一個可選的查詢參數 q,型別為 str
  • 一個查詢參數 skip,型別為 int,預設為 0
  • 一個查詢參數 limit,型別為 int,預設為 100

兩種情況下,資料都會被轉換、驗證,並記錄到 OpenAPI schema 中等。

如何使用

現在你可以用這個類別來宣告你的相依性。

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

FastAPI 會呼叫 CommonQueryParams 類別。這會建立該類別的一個「實例」,而該實例會以參數 commons 的形式傳給你的函式。

型別註解與 Depends

注意上面程式碼裡我們寫了兩次 CommonQueryParams

commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

Tip

如有可能,優先使用 Annotated 版本。

commons: CommonQueryParams = Depends(CommonQueryParams)

最後面的 CommonQueryParams,在:

... Depends(CommonQueryParams)

...才是 FastAPI 實際用來知道相依性是什麼的依據。

FastAPI 會從這個物件中提取宣告的參數,並且實際呼叫它。


在這個例子中,前面的 CommonQueryParams,於:

commons: Annotated[CommonQueryParams, ...

Tip

如有可能,優先使用 Annotated 版本。

commons: CommonQueryParams ...

...對 FastAPI 來說沒有任何特殊意義。FastAPI 不會用它來做資料轉換、驗證等(因為這部分由 Depends(CommonQueryParams) 處理)。

其實你可以只寫:

commons: Annotated[Any, Depends(CommonQueryParams)]

Tip

如有可能,優先使用 Annotated 版本。

commons = Depends(CommonQueryParams)

...像是:

from typing import Annotated, Any

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons=Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

但仍建議宣告型別,這樣你的編輯器就知道會以何種型別作為參數 commons 傳入,進而幫助你做自動完成、型別檢查等:

捷徑

不過你會發現這裡有些重複程式碼,我們寫了兩次 CommonQueryParams

commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

Tip

如有可能,優先使用 Annotated 版本。

commons: CommonQueryParams = Depends(CommonQueryParams)

FastAPI 為這類情況提供了一個捷徑:當相依性「明確」是一個類別,且 FastAPI 會「呼叫」它來建立該類別的實例時。

對這些特定情況,你可以這樣做:

不要寫:

commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

Tip

如有可能,優先使用 Annotated 版本。

commons: CommonQueryParams = Depends(CommonQueryParams)

...而是改為:

commons: Annotated[CommonQueryParams, Depends()]

Tip

如有可能,優先使用 Annotated 版本。

commons: CommonQueryParams = Depends()

你把相依性宣告為參數的型別,並使用不帶任何參數的 Depends(),而不用在 Depends(CommonQueryParams) 裡「再」寫一次整個類別。

整個範例就會變成:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends()]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

...而 FastAPI 會知道該怎麼做。

Tip

如果你覺得這樣比幫助更令人困惑,那就忽略它吧,你並不「需要」這個技巧。

這只是個捷徑。因為 FastAPI 在意幫你減少重複的程式碼。