额外的数据模型¶
接续前面的例子,通常会有多个相关的模型。
这在用户模型中尤其常见,因为:
- 输入模型需要包含密码。
- 输出模型不应包含密码。
- 数据库模型可能需要包含哈希后的密码。
多个模型¶
以下是这些模型及其密码字段和使用场景的大致概念:
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_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(它是一个 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_hasher 和 fake_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.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 3.10 中的 Union¶
在这个例子中,我们将 Union[PlaneItem, CarItem] 作为参数 response_model 的值传递。
因为我们是将其作为参数的值传递,而不是放在类型注解中,所以即使在 Python 3.10 中,我们也必须使用 Union。
如果它在类型注解中,我们本可以使用竖线,如:
some_variable: PlaneItem | CarItem
但如果我们在赋值语句 response_model=PlaneItem | CarItem 中这样做,就会得到一个错误,因为 Python 会尝试在 PlaneItem 和 CarItem 之间执行一个无效的操作,而不是将其解释为类型注解。
模型列表¶
同样,你也可以声明包含对象列表的响应。
为此,请使用标准的 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 模型并自由继承。
如果一个实体必须能够有不同的“状态”,你不需要为每个实体只用一个数据模型。就像用户“实体”有包含 password、password_hash 和不含密码的状态一样。