跳至內容

額外模型

延續前面的例子,通常會有多個相關的模型。

尤其是使用者模型,因為

  • 輸入模型 需要能夠包含密碼。
  • 輸出模型 不應該包含密碼。
  • 資料庫模型 可能需要包含雜湊後的密碼。

危險

永遠不要儲存使用者的明文密碼。請務必儲存可以驗證的「安全雜湊」。

如果您不知道,「密碼雜湊」是什麼,您將在安全章節中學到。

多個模型

以下是如何使用密碼欄位以及它們使用位置的模型的大致概念

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.dict(), 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
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Union[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.dict(), 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

資訊

在 Pydantic v1 中,該方法稱為 .dict(),在 Pydantic v2 中已棄用(但仍支援),並重新命名為 .model_dump()

此處的範例使用 .dict() 與 Pydantic v1 相容,但如果您可以使用 Pydantic v2,則應改用 .model_dump()

關於 **user_in.dict()

Pydantic 的 .dict()

user_inUserIn 類別的 Pydantic 模型。

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

因此,如果我們像這樣建立一個 Pydantic 物件 user_in

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

然後我們呼叫

user_dict = user_in.dict()

我們現在在變數 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.dict() 獲得 user_dict,這段程式碼

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

相當於

UserInDB(**user_in.dict())

...因為 user_in.dict() 是一個 dict,然後我們透過在 UserInDB 前加上 ** 將其傳遞給 Python 來「展開」它。

因此,我們從另一個 Pydantic 模型的資料中獲得一個 Pydantic 模型。

展開 dict 和額外的關鍵字

然後新增額外的關鍵字引數 hashed_password=hashed_password,就像在

UserInDB(**user_in.dict(), 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,
)

警告

支援的附加函式 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.dict(), 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
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[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.dict(), 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

注意事項

定義 Union 時,請先包含最特定的類型,然後再包含較不特定的類型。在以下範例中,更特定的 PlaneItem 位於 Union[PlaneItem, CarItem]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]
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 之間執行無效操作,而不是將其解釋為類型註釋。

模型列表

同樣地,您可以宣告物件列表的回應。

為此,請使用標準 Python typing.List(或在 Python 3.9 及更高版本中僅使用 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
from typing import 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

具有任意 dict 的回應

您也可以使用普通的任意 dict 宣告回應,只宣告鍵和值的類型,而不使用 Pydantic 模型。

如果您事先不知道有效的欄位/屬性名稱(Pydantic 模型需要),這會很有用。

在這種情況下,您可以使用 typing.Dict(或在 Python 3.9 及更高版本中僅使用 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}
from typing import 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}

摘要

針對每種情況使用多個 Pydantic 模型並自由繼承。

如果實體必須能夠具有不同的「狀態」,則您不需要為每個實體都有一個資料模型。例如,使用者「實體」的狀態包括 passwordpassword_hash 和無密碼。