书接上文,多个关联模型这种情况很常见。
特别是用户模型,因为:
- 输入模型应该含密码
- 输出模型不应含密码
- 数据库模型需要加密的密码
千万不要存储用户的明文密码。始终存储可以进行验证的安全哈希值。
如果不了解这方面的知识,请参阅安全性中的章节,了解什么是密码哈希。
多个模型
下面的代码展示了不同模型处理密码字段的方式,及使用位置的大致思路:
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
**user_in.dict()
简介
Pydantic 的 .dict()
user_in
是类 UserIn
的 Pydantic 模型。
Pydantic 模型支持 .dict()
方法,能返回包含模型数据的字典。
因此,如果使用如下方式创建 Pydantic 对象 user_in
:
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
就能以如下方式调用:
user_dict = user_in.dict()
现在,变量 user_dict
中的就是包含数据的字典(变量 user_dict
是字典,不是 Pydantic 模型对象)。
以如下方式调用:
print(user_dict)
输出的就是 Python 字典:
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
解包 dict
把字典 user_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()
是字典,在传递给 UserInDB
时,把 **
加在 user_in.dict()
前,可以让 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,
)
警告
辅助的附加函数只是为了演示可能的数据流,但它们显然不能提供任何真正的安全机制。
减少重复
FastAPI 的核心思想就是减少代码重复。
代码重复会导致 bug、安全问题、代码失步等问题(更新了某个位置的代码,但没有同步更新其它位置的代码)。
上面的这些模型共享了大量数据,拥有重复的属性名和类型。
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
Union
或者 anyOf
响应可以声明为两种类型的 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 typing.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
:
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 模型继承定义的基类。
实体必须具有不同的状态时,不必为不同状态的实体单独定义数据模型。例如,用户实体就有包含 password
、包含 password_hash
以及不含密码等多种状态。