15_额外的模型

15_额外的模型

我们继续之前的例子,可见,代码中拥有多个相关的模型是很常见的。

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

  • 输入模型需要拥有密码属性
  • 输出模型不应该包含密码
  • 数据库模型很可能需要保存密码的哈希值

安全提示:

永远不要存储用户的明文密码。始终存储一个可以用于验证的「安全哈希值」。

如果你尚未了解该知识,你可以在安全章节中学习何为「密码哈希值」。


1. 多个模型:

下面是应该如何根据它们的密码字段以及使用位置去定义模型的大概思路:

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

关于 **user_in.dict():

1). Pydantic 的 .dict()

user_in 是一个 UserIn 类的 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而不是Pydantic模型对象)。

如果我们调用

print(user_dict)

将会获得一个这样的Python dict:

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}
2). 解包 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_idct 来表示将来可能包含的任何内容:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)
3). 来自于其他模型内容的 Pydantic 模型:

在上面,我们从 user_in.dict() 中获得了 user_dict

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

这两行代码等同于:

UserInDB(**user_in.dict())

因为 user_in.dict() 是一个 dict,然后我们通过 **开头传递给UserInDB类来解包它。

这样,我们获得了一个来自于其他 Pydantic 模型中数据内容的 Pydantic 模型。

4). 解包 dict 和额外关键字:

然后, 我们在UserInDB类的实例化时添加了额外的关键字参数 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_*()只是演示可能的数据流,但它们当然并没有提供任何真正的安全性。


2. 减少重复:

减少代码重复是FastAPI的核心思想之一。

因为代码重复会增加出现bug、安全问题、代码不同步(当你在一个位置更新了代码但没有在其他位置更新)等问题的可能性。

上面的模型中包含大量的共享数据,并拥有重复的属性名称和类型。我们可以对其进行改进。

我们可以声明一个 UserBase 模型作为其他模型的基类。然后创建继承该模型属性(类型声明、校验等)的子类。

所有的数据转换、校验、文档生成等仍能正常运行。

这样,我们可以仅声明模型之间有差异的部分(具有明文密码、具有密码哈希值以及不包含密码):

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


3. Union或者anyOf

我们可以将响应声明为一个可能为两种类型的 Union,这意味着该响应将是两种类型中的任何一种。

这将在 OpenAPI 中使用 anyOf 进行定义。

为此,请使用Python标准的类型提示 typing.Union(即使使用了python 3.10,也要调用 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 = "car"


class PlaneItem(BaseItem):  # 更详细
    type = "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])  # 使用Union指定响应模型
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会尝试在PlaneItemCartItem之间执行一个无效的操作,而不是将其解释为类型注释。


4. 模型列表:

可以用同样的方式声明由对象列表构成的响应。

为此,请使用标准的 Python typing.List 或者在 python 3.9及之后版本中直接使用 list

python 3.9+:

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

python3.6+:

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

5. 任意dict构成的响应:

我们还可以使用一个任意的普通 dict 声明响应,仅声明键和值的类型,而不使用 Pydantic 模型。

如果你事先不知道有效的 字段/属性 名称(对于 Pydantic 模型是必需的),这将很有用。

在这种情况下,你可以使用 typing.Dict, 或者在Python 3.9 及之后版本中直接使用 dict

python3.9+:

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}

python3.6+:

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 模型,并针对不同场景自由地继承。

如果一个实体(entity)必须能够具有不同的「状态 states」,你无需为每个状态的实体定义单独的数据模型。以用户「实体」为例,其状态有包含 password、包含 password_hash 以及不含密码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值