跳至内容

额外模型

继续前面的示例,通常会有多个相关的模型。

这在用户模型中尤其常见,因为

  • **输入模型** 需要能够包含密码。
  • **输出模型** 不应该包含密码。
  • **数据库模型** 可能需要包含一个经过哈希处理的密码。

危险

切勿存储用户的明文密码。始终存储“安全哈希”,然后您可以验证该哈希。

如果您不知道,您将在安全章节中了解“密码哈希”是什么。

多个模型

以下是如何使用其密码字段以及它们在何处使用的模型的总体思路

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和无密码的状态。