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_fields与FieldInfo.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)请求体、表单可选编辑、测试夹具的灵活构造等。
实现要点有三步:
- 遍历原模型的
model_fields类属性,拿到每个字段的FieldInfo实例; - 用
FieldInfo.asdict()把字段信息拆成"注解 + 元数据 + 属性"三部分; - 用
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:剩余字段级属性到值的映射(如alias、title等)。
例如对于如下模型:
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字段f的FieldInfo.asdict()结果将是:
annotation:int;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, )- 以原模型作为基类,可以继承其 校验器、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, )- 同 3.1 版本:以原模型为基类继承校验器、computed fields 等,父类字段被我们定义的新字段覆盖。
四、逐行拆解:新注解是如何"重建"出来的
官方示例给出了新注解的构造模板,理解它就能理解整个工厂函数:
new_annotation = Annotated[ f_dct['annotation'] | None, # (1)! *f_dct['metadata'], # (2)! Field(**f_dct['attributes']), # (3)! ]三个组成部分各有分工:
f_dct['annotation'] | None:基于原有注解追加None作为允许值,使字段变成可选。在我们的例子中,这等价于int | None;*f_dct['metadata']:解包并复用原有元数据。在我们的例子中,等价于在Annotated中显式写出Field(gt=1)与WithJsonSchema({'extra': 'data'})两个元数据项;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,完整实现了"从已有模型派生全可选字段模型"的工厂函数。核心知识链条为:
create_model()接受字段定义二元组(类型 + 默认值/Field()),并支持__base__、__config__、__validators__等定制参数(pydantic/main.py);model_fields暴露每个字段的FieldInfo,asdict()将其拆解为annotation/metadata/attributes三部分(pydantic/fields.py);- 用
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),仅供参考