更多模型¶
🌐 Translation by AI and humans
This translation was made by AI guided by humans. 🤝
It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖
You can improve this translation by helping us guide the AI LLM better.
书接上文,多个关联模型这种情况很常见。
特别是用户模型,因为:
- 输入模型应该含密码
- 输出模型不应含密码
- 数据库模型可能需要包含哈希后的密码
多个模型¶
下面的代码展示了不同模型处理密码字段的方式,及使用位置的大致思路:
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_in 是类 UserIn 的 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()
现在,变量 user_dict 中的是包含数据的 dict(它是 dict,不是 Pydantic 模型对象)。
以如下方式调用:
print(user_dict)
输出的就是 Python dict:
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
解包 dict¶
把 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.model_dump() 中得到了 user_dict,下面的代码:
user_dict = user_in.model_dump()
UserInDB(**user_dict)
等效于:
UserInDB(**user_in.model_dump())
……因为 user_in.model_dump() 是 dict,在传递给 UserInDB 时,把 ** 加在 user_in.model_dump() 前,可以让 Python 进行解包。
这样,就可以用其它 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,
)
Warning
配套的辅助函数 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.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
Union 或 anyOf¶
响应可以声明为两个或多个类型的 Union,即该响应可以是这些类型中的任意一种。
在 OpenAPI 中会用 anyOf 表示。
为此,请使用 Python 标准类型提示 typing.Union:
Note
定义 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]
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
🤓 Other versions and variants
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}
🤓 Other versions and variants
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 以及不含密码等多种状态。