ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FastAPI 进阶:多 Pydantic 模型(Extra Models)实战 —— 用“输入 / 输出 / 数据库“三套模型组织接口

FastAPI 进阶:多 Pydantic 模型(Extra Models)实战 —— 用“输入 / 输出 / 数据库“三套模型组织接口 FastAPI 进阶多 Pydantic 模型Extra Models实战 —— 用输入 / 输出 / 数据库三套模型组织接口【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi本文源自本仓库 日文官方教程User Guidedocs/ja/docs/tutorial/extra-models.md对应的英文原文为 docs/en/docs/tutorial/extra-models.md。教程讲述的是 FastAPI 开发中最常见的建模场景同一业务实体如用户往往需要在不同环节呈现不同字段因此需要围绕输入、输出与数据库存储声明多套 Pydantic 模型并在它们之间高效地转换数据。读完本文你将掌握三种模型的拆分方式、**user_in.model_dump()字典展开的精髓、用继承消除重复建模、用Union/list/dict灵活声明响应模型并能结合仓库中的示例代码docs_src/extra_models/与测试用例tests/test_tutorial/test_extra_models/验证全部行为。为什么一个实体需要多个模型在真实的 API 设计中同一实体在不同阶段所需的字段往往不同。以用户为例输入模型Input model需要携带password用于接收客户端提交的注册 / 创建请求输出模型Output model绝不能包含密码避免把敏感字段泄漏给客户端数据库模型Database model需要存储的是hashed_password哈希后的密码而不是明文密码。这与一个实体只对应一个模型的直觉相悖因此 FastAPI 教程专门用一章来讲如何组织这些额外模型。在动手之前有一条必须时刻遵守的安全底线原文以danger级别告警强调**永远不要保存用户的明文密码。**请始终保存一个你可以用来校验的安全哈希。 如果还不了解密码哈希password hash是什么可以在安全章节的 Password Hashing 小节学习。本文后续示例中的fake_password_hasher与fake_save_user仅用于演示数据的流动过程并不提供任何真实的安全保护生产代码必须替换为诸如 passlib/argon2 之类的正规哈希方案。三个模型、一条路由完整实现先看第一种写法直接定义三个互不相干的模型。完整代码位于 docs_src/extra_models/tutorial001_py310.pyfrom 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_passwordhashed_password) print(User saved! ..not really) return user_in_db app.post(/user/, response_modelUserOut) async def create_user(user_in: UserIn): user_saved fake_save_user(user_in) return user_saved三个模型的字段分工如下模型字段用途UserInusername、password、email、full_name请求体校验接收明文密码UserOutusername、email、full_name响应模型response_modelUserOut过滤掉密码UserInDBusername、hashed_password、email、full_name模拟数据库中的已哈希记录路由处理流程请求到达POST /user/→ 按UserIn校验并生成user_in→fake_save_user用fake_password_hasher对明文密码做一次伪造的哈希 → 通过UserInDB(**user_in.model_dump(), hashed_passwordhashed_password)构建数据库模型 → 返回给fake_save_user的调用方最后 FastAPI 依据response_modelUserOut输出响应——password与hashed_password都不会出现在响应 JSON 中。EmailStr是 Pydantic 提供的带格式校验的邮箱类型需额外安装email-validator包full_name: str | None None使用 Python 3.10 起的|语法声明可选字段。仓库中的测试 tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py 精确验证了这一点test_postPOST 携带{username: johndoe, password: secret, email: johndoeexample.com, full_name: John Doe}后断言响应status_code 200且返回 JSON只包含username/email/full_namepassword确实被过滤test_openapi_schema请求体 schema 引用UserIn响应 200 schema 引用UserOutOpenAPI 组件中UserIn的required含password而UserOut的required只有[username, email]。启动该示例同样简单本仓库所有示例均基于 Python 3.10故文件名为_py310uvicorn docs_src.extra_models.tutorial001_py310:app --reload核心语法拆解**user_in.model_dump()把两个模型串起来的魔法就藏在这一行里UserInDB(**user_in.model_dump(), hashed_passwordhashed_password)它由四个层次叠加而成逐一拆开看1. Pydantic 的.model_dump()user_in是UserIn类的 Pydantic 模型实例。Pydantic 模型自带.model_dump()方法返回一个包含模型全部数据的dict本仓库基于 Pydantic v2若使用 Pydantic v1则对应方法是.dict()。例如创建如下对象user_in UserIn(usernamejohn, passwordsecret, emailjohn.doeexample.com)然后调用user_dict user_in.model_dump()此时user_dict就是一个字典而非 Pydantic 对象。打印它print(user_dict)输出{ username: john, password: secret, email: john.doeexample.com, full_name: None, }注意未显式传入的full_name也会以默认值None出现在字典中。2. Python 的字典展开Unpacking**把一个字典用**传给函数或类时Python 会把它展开字典的每个键值对被直接作为关键字参数传入。因此下面的写法UserInDB(**user_dict)等价于UserInDB( usernamejohn, passwordsecret, emailjohn.doeexample.com, full_nameNone, )更准确地说等价于逐项取出UserInDB( username user_dict[username], password user_dict[password], email user_dict[email], full_name user_dict[full_name], )好处在于即使未来user_dict增加了内容**也会原样透传无需手写每一项。3. 从另一个模型的内容构建新模型上例中user_dict来自user_in.model_dump()所以两行代码user_dict user_in.model_dump() UserInDB(**user_dict)与一行代码完全等价UserInDB(**user_in.model_dump())因为user_in.model_dump()返回的本来就是dict加上**前缀让 Python 展开它即可。这样我们就用一个 Pydantic 模型的数据构造出了另一个 Pydantic 模型。4. 字典展开 追加关键字参数UserInDB还需要数据库模型独有的hashed_password字段此时只需在展开之后追加一个关键字参数UserInDB(**user_in.model_dump(), hashed_passwordhashed_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(supersecret raw_password)这类写法只是为了让数据流向可演示不具备任何真实安全性切勿照搬进生产代码。消除重复用继承抽出一个UserBase上面的三个模型其实共享了大量字段username、email、full_name及各自的类型声明与校验规则。而减少代码重复正是 FastAPI 的核心设计理念之一——重复代码越多越容易出现 bug、安全问题以及改了一处忘了改另一处的代码失同步问题。更好的做法是先声明一个作为基座的UserBase模型再让它各个子类去继承属性类型声明、校验规则等。数据转换、校验、自动文档等一切能力照常工作——需要声明的只是模型之间的差异。改写后的完整代码位于 docs_src/extra_models/tutorial002_py310.pyfrom 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_passwordhashed_password) print(User saved! ..not really) return user_in_db app.post(/user/, response_modelUserOut) async def create_user(user_in: UserIn): user_saved fake_save_user(user_in) return user_saved现在每个模型只保留自己的独有字段UserBaseusernameemailfull_name公共底座UserIn(UserBase)仅追加明文passwordUserOut(UserBase)pass直接继承全部公共字段不输出任何密码UserInDB(UserBase)仅追加hashed_password。路由函数与fake_save_user完全不用改动。仓库测试 test_tutorial001_tutorial002.py 同时参数化覆盖了 tutorial001 与 tutorial002 两个版本断言两者对外行为与生成的 OpenAPI schema 完全一致——这正好证明继承版在不改变接口契约的前提下消灭了字段层面的重复。Union/anyOf响应可能是多种模型之一有时一个接口的响应可能是两种或更多类型中的任意一种。FastAPI 允许把响应声明为这些类型的Union并在 OpenAPI 中生成anyOf。使用标准库的typing.Union或 Python 3.10 起的|即可实现。本仓库示例 docs_src/extra_models/tutorial003_py310.py 演示了同一物品字典要么是汽车、要么是飞机的场景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, its my aeroplane, type: plane, size: 5, }, } app.get(/items/{item_id}, response_modelPlaneItem | CarItem) async def read_item(item_id: str): return items[item_id]BaseItem定义公共字段description与typeCarItem(BaseItem)把type默认成carPlaneItem(BaseItem)把type默认成plane并额外要求size: intitem1会被校验为CarItemitem2会被校验为PlaneItem。关于Union有一个值得记住的排序建议原文以note提示定义Union时请把最具体的类型放前面随后再放更宽泛的类型。例如下面示例中更具体的PlaneItem在Union[PlaneItem, CarItem]或PlaneItem | CarItem中排在了CarItem之前。type的默认值使得 Pydantic 能据此区分分支而排序关系到模式校验/文档的优先级。仓库测试 tests/test_tutorial/test_extra_models/test_tutorial003.py 给出了可验证的证据其test_openapi_schema快照显示OpenAPI 响应 schema 生成的是anyOf: [ { $ref: #/components/schemas/PlaneItem }, { $ref: #/components/schemas/CarItem } ]即声明顺序被 1:1 映射为anyOf分支顺序而test_get_car/test_get_plane分别断言两种物品都能被正确序列化返回。关于 Python 3.10 的Union说明这里有一个容易踩坑的语法细节。教程原文的说明是示例把Union[PlaneItem, CarItem]作为response_model参数的取值来传递而不是写进类型注解里因此在较老的 Python 版本上即使使用 Python 3.10 也必须借助Union而不能写PlaneItem | CarItem——因为如果写在类型注解里Python 允许用竖线|some_variable: PlaneItem | CarItem但若把它放进赋值response_modelPlaneItem | CarItemPython 会把|当作在PlaneItem与CarItem两个类对象之间执行的运算符而不是类型注解语法。需要补充的是从 Python 3.10 开始PEP 604type对象本身支持__or__运算符PlaneItem | CarItem作为表达式在运行时也能成立会得到types.UnionType。本仓库的_py310示例正是基于这一点直接在装饰器中写作response_modelPlaneItem | CarItem并与旧式typing.Union生成同样的 OpenAPIanyOf。因此结论是追求最大兼容性如 Python 3.8/3.9时使用typing.Union仅在 Python 3.10 环境且需要把联合类型作为参数值时才可像本仓库示例那样直接用|。模型列表response_modellist[Item]同样的思路可以扩展到一组对象。把响应声明为模型的list即可示例见 docs_src/extra_models/tutorial004_py310.pyfrom 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: Its my aeroplane}, ] app.get(/items/, response_modellist[Item]) async def read_items(): return items这里使用 Python 3.9 起支持内建泛型下标的标准list即list[Item]。FastAPI 会为数组中的每一个元素做Item校验、序列化与输出过滤返回 JSON 即[ {name: Foo, description: There comes my hero}, {name: Red, description: Its my aeroplane} ]与之对应的自动化测试见 tests/test_tutorial/test_extra_models/test_tutorial004.py。不定义模型用任意dict直接声明响应最后一类场景更自由当预先不知道合法的字段/属性名时Pydantic 模型要求字段名固定可以用只声明键类型与值类型的普通dict作为响应模型。示例见 docs_src/extra_models/tutorial005_py310.pyfrom fastapi import FastAPI app FastAPI() app.get(/keyword-weights/, response_modeldict[str, float]) async def read_keyword_weights(): return {foo: 2.3, bar: 3.4}键类型为str值类型为float接口可以返回任意数量、任意名字的关键字及其权重只要值符合类型约束非常适合标签云、词频统计、打分表这类键集合在运行时才确定的数据。测试 tests/test_tutorial/test_extra_models/test_tutorial005.py 的 OpenAPI 快照揭示了它的底层表达响应 schema 为type: object且带additionalProperties: {type: number}——也就是说 OpenAPI 用它表达对象的值都是数字。注意旧写法Dict[str, float]来自typing在 Python 3.9 中同样可以换成内建dict[str, float]。小结把本节方法论浓缩成一句话与教程原文的 Recap 一致**为一个用例随意使用多个 Pydantic 模型并自由继承。**如果某个实体必须呈现多种不同状态完全不必只给它定义一个数据模型——正如用户实体的三种状态含明文password、含hashed_password、两者都不含。落实到代码上记住四条可复用的套路即可输入 / 输出 / 数据库分离接收请求、返回响应、落库存储各用一套模型让密码永不越界继承抽公共字段用UserBase之类的基类持有共享属性子类只声明差异转换 / 校验 / 文档全部照常工作**user_in.model_dump()把一个模型的数据展开注入另一个模型必要时再追加hashed_password...这类额外关键字灵活声明响应单值类型不够用时用Union/|anyOf、list[Model]、dict[str, float]覆盖多选一 / 多选多 / 任意字典三种形态。上述每个结论都可以在本仓库中逐一验证示例源码在 docs_src/extra_models/tutorial001_py310.py~tutorial005_py310.py行为与 OpenAPI 契约由 tests/test_tutorial/test_extra_models/ 下的四个测试文件以快照方式锁定。若想了解response_model的更完整语义过滤、排除等进阶参数可继续阅读日文教程《レスポンスモデル》或英文版 Response Model。【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表