Skip to content

額外的模型

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

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

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

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

英文版

延續前一個範例,通常會有不只一個彼此相關的模型。

對使用者模型尤其如此,因為:

  • 「輸入模型」需要能包含密碼。
  • 「輸出模型」不應包含密碼。
  • 「資料庫模型」通常需要儲存雜湊後的密碼。

Danger

切勿儲存使用者的明文密碼。務必只儲存可供驗證的「安全雜湊」。

若你還不清楚,稍後會在安全性章節學到什麼是「密碼雜湊」。

多個模型

以下是模型大致長相、與其密碼欄位以及它們被使用的位置:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

關於 **user_in.model_dump()

Pydantic 的 .model_dump()

user_in 是一個 UserIn 類別的 Pydantic 模型。

Pydantic 模型有 .model_dump() 方法,會回傳包含該模型資料的 dict

因此,若我們建立一個 Pydantic 物件 user_in 如:

user_in = UserIn(username="john", password="secret", email="john.doe@example.com")

接著呼叫:

user_dict = user_in.model_dump()

此時變數 user_dict 會是一個承載資料的 dict(也就是 dict,而非 Pydantic 模型物件)。

若再呼叫:

print(user_dict)

我們會得到一個 Python dict

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}

解包 dict

若將像 user_dict 這樣的 dict**user_dict 傳給函式(或類別),Python 會將其「解包」,把 user_dict 的鍵和值直接當作具名引數傳入。

因此,延續上面的 user_dict,寫成:

UserInDB(**user_dict)

效果等同於:

UserInDB(
    username="john",
    password="secret",
    email="john.doe@example.com",
    full_name=None,
)

更精確地說,直接使用 user_dict(未來內容可能有所不同)則等同於:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

由另一個模型內容建立 Pydantic 模型

如上例我們從 user_in.model_dump() 得到 user_dict,以下程式碼:

user_dict = user_in.model_dump()
UserInDB(**user_dict)

等同於:

UserInDB(**user_in.model_dump())

...因為 user_in.model_dump() 回傳的是 dict,接著在傳給 UserInDB 時以 ** 前綴讓 Python 進行解包。

因此,我們可以用一個 Pydantic 模型的資料建立另一個 Pydantic 模型。

解包 dict 並加入額外參數

接著加入額外的具名引數 hashed_password=hashed_password,如下:

UserInDB(**user_in.model_dump(), hashed_password=hashed_password)

...結果等同於:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)

Warning

輔助函式 fake_password_hasherfake_save_user 只是用來示範資料流程,並不提供任何實際的安全性。

減少重複

減少程式碼重複是 FastAPI 的核心理念之一。

因為重複的程式碼會提高發生錯誤、安全性問題、程式不同步(某處更新但其他處未更新)等風險。

而這些模型共享大量資料,重複了屬性名稱與型別。

我們可以做得更好。

我們可以宣告一個作為基底的 UserBase 模型,其他模型繼承它成為子類別,沿用其屬性(型別宣告、驗證等)。

所有資料轉換、驗證、文件產生等仍可正常運作。

如此一來,我們只需要宣告模型之間的差異(含明文 password、含 hashed_password、或不含密碼):

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

UnionanyOf

你可以將回應宣告為多個型別的 Union,表示回應可能是其中任一型別。

在 OpenAPI 中會以 anyOf 定義。

要達成這點,使用標準的 Python 型別提示 typing.Union

Note

在定義 Union 時,請先放置「更具體」的型別,再放「較不具體」的型別。以下範例中,較具體的 PlaneItem 置於 CarItem 之前:Union[PlaneItem, CarItem]

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]

Python 3.10 中的 Union

此範例中,我們將 Union[PlaneItem, CarItem] 作為引數 response_model 的值。

由於這裡是把它當作引數的「值」傳入,而非用於型別註記,因此即使在 Python 3.10 也必須使用 Union

若用於型別註記,則可以使用直線(|),如下:

some_variable: PlaneItem | CarItem

但若寫成指定值 response_model=PlaneItem | CarItem 會發生錯誤,因為 Python 會嘗試在 PlaneItemCarItem 之間執行「無效運算」,而非將其視為型別註記。

模型的清單

同樣地,你可以將回應宣告為物件的 list

為此,使用標準的 Python list

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=list[Item])
async def read_items():
    return items
🤓 Other versions and variants
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=list[Item])
async def read_items():
    return items

以任意 dict 作為回應

你也可以用一般的任意 dict 宣告回應,只需指定鍵和值的型別,而不必使用 Pydantic 模型。

當你事先不知道可用的欄位/屬性名稱(定義 Pydantic 模型所需)時,這很實用。

此時可使用 dict

from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}
🤓 Other versions and variants
from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

重點回顧

依情境使用多個 Pydantic 模型並靈活繼承。

當一個實體需要呈現不同「狀態」時,不必侷限於一個資料模型。例如使用者這個實體,可能有包含 password、包含 password_hash,或不含密碼等不同狀態。