跳到内容

额外模型

延续之前的示例,拥有多个相关模型是很常见的。

对于用户模型来说尤其如此,因为:

  • 输入模型需要包含密码。
  • 输出模型不应包含密码。
  • 数据库模型可能需要包含哈希后的密码。

危险

永远不要存储用户的明文密码。始终存储你可以进行验证的“安全哈希”。

如果你还不了解,你将在安全章节中学习什么是“密码哈希”。

多个模型

这里有一个通用构思,展示了模型及其密码字段在不同场景下的使用方式:

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_inUserIn 类的一个 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()

我们现在得到了一个包含数据的 dict,变量名为 user_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,我们通过 ** 前缀让 Python 将其“解包”传递给 UserInDB

因此,我们从另一个 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,
)

警告

辅助函数 fake_password_hasherfake_save_user 仅用于演示数据流向,当然它们并不提供任何真正的安全性。

减少重复

减少代码重复是 FastAPI 的核心理念之一。

代码重复会增加产生 Bug、安全问题、代码不同步问题(你在一个地方更新了,但另一个地方忘了)等的几率。

这些模型共享了大量数据,重复定义了属性名称和类型。

我们可以做得更好。

我们可以声明一个 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

注意

定义 Union 时,先包含最具体的类型,然后是较不具体的类型。在下面的示例中,更具体的 PlaneItemUnion[PlaneItem, CarItem] 中位于 CarItem 之前。

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=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 标准的 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 模型中是必需的),这将非常有用。

在这种情况下,你可以使用 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 和不含密码。