news 2026/9/11 6:39:32

pydantic 动态模型创建实战:基于 `create_model()` 派生可选字段模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pydantic 动态模型创建实战:基于 `create_model()` 派生可选字段模型

pydantic 动态模型创建实战:基于create_model()派生可选字段模型

【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic

导读

在数据校验场景中,我们经常需要根据运行时信息动态生成模型,例如把现有模型的全部字段派生为可选字段(用于"部分更新"等场景)。本篇文章以 pydantic 官方示例 docs/examples/dynamic_models.md 为骨架,深入讲解如何借助create_model()工厂函数、model_fieldsFieldInfo.asdict()从已有模型动态派生新模型,并给出完整可运行的代码、Python 3.10 / 3.11+ 两种写法、类型标注技巧与实验性MISSING哨兵方案。读完本文,你将掌握一套可复制的"字段可选化"工厂函数实现,并理解 pydantic 动态模型创建的底层机制。

一、场景与思路:为什么要"动态派生"模型

Pydantic 支持通过create_model()工厂函数在运行时动态创建模型。官方文档 docs/concepts/models.md 给出了最基础的用法:

from pydantic import BaseModel, create_model DynamicFoobarModel = create_model('DynamicFoobarModel', foo=str, bar=(int, 123)) # 等价于: class StaticFoobarModel(BaseModel): foo: str bar: int = 123

字段定义以关键字参数传入,可以是两种形式:

  • 单元素:仅表示字段的类型注解,例如foo=str
  • 二元组:第一个元素是类型,第二个元素是赋值(默认值或Field()函数调用),例如bar=(int, 123)

在此基础上,create_model()还支持一系列以双下划线开头的特殊关键字参数,用于定制新模型的行为(对应源码 pydantic/main.py 中的函数签名):

参数说明
__config__新模型的ConfigDict配置(与__base__互斥,同时传入会抛出PydanticUserError
__doc__新模型的 docstring
__base__新模型的基类或基类元组
__module__新模型所属模块名,缺省时取自调用方栈帧
__validators__字段校验器字典,键为校验器方法名,值为校验器函数
__cls_kwargs__类创建时的额外关键字参数(如metaclass
__qualname__新模型的限定名
**field_definitions字段定义(单元素或二元组)

注意:根据 pydantic/main.py 中的源码警告,create_model()会执行字段注解中可能包含的任意代码(例如字符串引用需要被求值时),在接收不可信输入时应谨慎使用。

二、核心目标:把模型的所有字段变成可选

官方示例 docs/examples/dynamic_models.md 的目标非常明确:从一个已有模型动态派生一个新模型,使其每一个字段都变为可选。典型应用场景包括:构建"部分更新"(PATCH)请求体、表单可选编辑、测试夹具的灵活构造等。

实现要点有三步:

  1. 遍历原模型的model_fields类属性,拿到每个字段的FieldInfo实例;
  2. FieldInfo.asdict()把字段信息拆成"注解 + 元数据 + 属性"三部分;
  3. Annotated重新组装出一个"允许None"的新注解,并通过create_model()传入。

2.1model_fields:字段信息的入口

BaseModel.model_fields是一个类属性字典,键是字段名,值是对应的FieldInfo实例。它包含了校验、序列化与 JSON Schema 生成所需的全部字段元数据,是动态派生模型时读取"原始字段定义"的标准入口。

2.2FieldInfo.asdict():字段信息的可编程表示

源码 pydantic/fields.py 对asdict()的定义非常清晰:它返回一个包含三个键的字典:

  • annotation:字段的类型注解;
  • metadata:类型约束与其他元数据组成的列表(如Annotated[int, Field(gt=1), WithJsonSchema(...)]中的[Gt(1), WithJsonSchema(...)]);
  • attributes:剩余字段级属性到值的映射(如aliastitle等)。

例如对于如下模型:

from typing import Annotated from pydantic import BaseModel, Field, WithJsonSchema class Model(BaseModel): f: Annotated[int, Field(gt=1), WithJsonSchema({'extra': 'data'}), Field(title='F')] = 1

字段fFieldInfo.asdict()结果将是:

  • annotationint
  • metadata[Gt(1), WithJsonSchema({'extra': 'data'})](类型约束与元数据列表);
  • attributes{'title': 'F'}(剩余字段属性)。

这样,字段的完整"配方"就被拆解成了可编程操作的三个部分,我们可以放心地"改造"后再重新组装。

三、工厂函数实现:make_fields_optional()

下面是官方示例给出的完整工厂函数。由于 Python 3.10 与 3.11+ 在Annotated语法上存在差异(3.10 使用元组写法,3.11+ 可直接用下标语法),示例提供了两个版本。

3.1 Python 3.10 版本

from typing import Annotated from pydantic import BaseModel, Field, create_model def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]: new_fields = {} for f_name, f_info in model_cls.model_fields.items(): f_dct = f_info.asdict() new_fields[f_name] = ( Annotated[(f_dct['annotation'] | None, *f_dct['metadata'], Field(**f_dct['attributes']))], None, ) return create_model( f'{model_cls.__name__}Optional', __base__=model_cls, # (1)! **new_fields, )
  1. 以原模型作为基类,可以继承其 校验器、computed fields 等特性;我们在new_fields中定义的字段会覆盖父类中的同名字段。

3.2 Python 3.11+ 版本

from typing import Annotated from pydantic import BaseModel, Field, create_model def make_fields_optional(model_cls: type[BaseModel]) -> type[BaseModel]: new_fields = {} for f_name, f_info in model_cls.model_fields.items(): f_dct = f_info.asdict() new_fields[f_name] = ( Annotated[f_dct['annotation'] | None, *f_dct['metadata'], Field(**f_dct['attributes'])], None, ) return create_model( f'{model_cls.__name__}Optional', __base__=model_cls, # (1)! **new_fields, )
  1. 同 3.1 版本:以原模型为基类继承校验器、computed fields 等,父类字段被我们定义的新字段覆盖。

四、逐行拆解:新注解是如何"重建"出来的

官方示例给出了新注解的构造模板,理解它就能理解整个工厂函数:

new_annotation = Annotated[ f_dct['annotation'] | None, # (1)! *f_dct['metadata'], # (2)! Field(**f_dct['attributes']), # (3)! ]

三个组成部分各有分工:

  1. f_dct['annotation'] | None:基于原有注解追加None作为允许值,使字段变成可选。在我们的例子中,这等价于int | None
  2. *f_dct['metadata']:解包并复用原有元数据。在我们的例子中,等价于在Annotated中显式写出Field(gt=1)WithJsonSchema({'extra': 'data'})两个元数据项;
  3. Field(**f_dct['attributes']):通过Field()函数把剩余的字段属性(如title='F')重新指定回去。

随后,create_model()的字段定义二元组中第二个元素被设置为None,作为新字段的默认值——这正是create_model()字段定义二元组的约定:第一个元素是类型,第二个元素是默认值或Field()调用(见 pydantic/main.py 的参数说明)。

五、运行演示:从"必填"到"全可选"

让我们用一个更简单的模型来验证工厂函数的效果:

from typing import Annotated from pydantic import BaseModel, Field class Model(BaseModel): a: Annotated[int, Field(gt=1)] ModelOptional = make_fields_optional(Model) m = ModelOptional() print(m.a) #> None

可以看到,派生出的ModelOptional无需提供任何字段即可实例化,字段a的默认值为None,且原有的gt=1约束依然被保留在新注解中。整个过程完全在运行时完成,无需手写新的模型类。

六、进阶细节与注意事项

6.1 类型标注:如何让静态检查更友好

工厂函数make_fields_optional()被定义为返回-> type[BaseModel]。如果你希望在静态类型检查时尽量保留原始类信息,可以使用类型变量:

Python 3.10+:

from typing import TypeVar ModelTypeT = TypeVar('ModelTypeT', bound=type[BaseModel]) def make_fields_optional(model_cls: ModelTypeT) -> ModelTypeT: ...

Python 3.12+(PEP 695 语法):

def make_fields_optional[ModelTypeT: type[BaseModel]](model_cls: ModelTypeT) -> ModelTypeT: ...

需要提醒的是:静态类型检查器无法理解"所有字段现在都是可选的"这一语义——类型检查器只会看到输入输出是同一个(或绑定的)模型类,不会知道字段的可选性发生了变化。这是动态派生的固有局限,在依赖严格类型检查的代码库中需要结合运行时行为来权衡。

6.2 用MISSING哨兵替代None作为默认值

如果"可选但区分'未提供'与'显式 None'"对业务有意义,pydantic 提供了实验性的MISSING哨兵(见 docs/concepts/experimental.md):

  • MISSING是单例对象,表示验证期间未提供该字段值;
  • 序列化时,值为MISSING的字段会从输出中排除;
  • 在 JSON Schema 中不会出现MISSING值。

在工厂函数中,只需把新注解与默认值里的None替换为MISSING

from pydantic.experimental.missing_sentinel import MISSING new_fields[f_name] = ( Annotated[f_dct['annotation'] | MISSING, *f_dct['metadata'], Field(**f_dct['attributes'])], MISSING, )

使用后,可以在业务侧通过field is MISSING判断"用户是否真的传了这个字段"。需要注意MISSING仍属实验特性,且包含MISSING值的模型暂不支持 pickle(详见 docs/concepts/experimental.md 的相关说明)。

6.3 为什么不建议直接复制并修改FieldInfo

你可能会想:直接把原模型的FieldInfo实例copy()一份、加个默认值再作为Annotated元数据复用,岂不是更简单?

官方示例明确给出了警告:这种做法虽然在部分情况下能工作,但不是受支持的模式,随时可能被破坏或废弃。原因在于FieldInfo是内部结构(slotted 类,且可能被第三方库子类化,见 pydantic/fields.py 的_copy()注释),直接对其做可变操作超出了公开 API 的保证范围。因此请务必采用本文推荐的"asdict()拆解 → 重建注解"模式。

七、更多应用:不止于"字段可选化"

官方文档指出,同样的模式可以推广到任何需要从现有模型派生新模型的场景,例如:

  • 移除默认值:把字段定义二元组的第二个元素从默认值改为...(必填);
  • 新增别名:在Field(**f_dct['attributes'])中追加alias=...
  • 调整约束:修改metadata列表中的约束项;
  • 组合多个基类:通过__base__传入元组,把多个模型的能力合并进新模型(与 docs/concepts/models.md 中的BarModel = create_model('BarModel', apple=(str, 'russet'), banana=(str, 'yellow'), __base__=FooModel)用法一致)。

同时,create_model()__validators__参数可以在动态模型中注入字段校验器(见 docs/concepts/models.md 中的UserModel示例),进一步扩展了动态建模的表达能力。

总结

本文基于官方示例 docs/examples/dynamic_models.md,完整实现了"从已有模型派生全可选字段模型"的工厂函数。核心知识链条为:

  1. create_model()接受字段定义二元组(类型 + 默认值/Field()),并支持__base____config____validators__等定制参数(pydantic/main.py);
  2. model_fields暴露每个字段的FieldInfoasdict()将其拆解为annotation/metadata/attributes三部分(pydantic/fields.py);
  3. Annotated[annotation | None, *metadata, Field(**attributes)]重建"可空化"注解,并把None(或MISSING)作为默认值回传给create_model()

这套模式既适用于 PATCH 场景的"部分更新",也可推广到别名调整、默认值修改等任意动态模型派生需求,是一份可以直接落地到业务代码中的实战方案。

【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 6:38:16

基于YOLO与大模型的电子元器件智能检测与识别系统实践

搞电子元器件检测这事,最早是我在实验室被逼出来的。一版板子贴了上百颗料,BOM清单里一半型号都是“未知”,只能拿放大镜对着丝印一颗颗查手册,眼睛都快瞎了。当时就想,能不能让电脑帮我干这活——用相机拍一张图&…

作者头像 李华
网站建设 2026/9/11 6:37:54

无影云电脑深度体验:从原理到实战的云端虚拟电脑完全指南

直接上结论:如果你最近一直在纠结“是不是有必要弄一台无影云电脑”,我的意见是先看完这篇再拍板。无影云电脑本质上就是一台放在云端的虚拟电脑,本地屏幕、键鼠、网络都只是入口,真正跑系统和软件的是远端机房里的虚拟机。我用它…

作者头像 李华
网站建设 2026/9/11 6:37:47

深入解析ML-KWS-for-MCU:Cortex-M上关键词识别的嵌入式AI工程架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 6:35:36

Redis缓存穿透、击穿与雪崩的防御实战

1. Redis缓存异常现象全景解读当我们在生产环境中使用Redis作为缓存层时,经常会遇到三种典型的异常场景:缓存穿透、击穿和雪崩。这些现象看似相似,实则有着本质区别。去年双十一大促期间,我所在团队的电商平台就曾因缓存雪崩导致服…

作者头像 李华
网站建设 2026/9/11 6:34:42

车载Android串口开发实战:RS485/Modbus/FT231X全链路避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 6:34:06

便携信号源实操指南:从手动设置到SCPI自动化测试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华