WheatField
WheatField

Pydantic TypeAdapter 类型适配器

November 25, 2024904 words, 5 min read
Authors

核心作用:

  • 适配器:即类型转换,允许开发者对输入数据进行转换和层次化处理,即支持普通类型及嵌套类型的类型转换。
  • 类型检验与序列化:可以用来对任意 Python 类型(包括非 BaseModel 类型)实现强类型校验和序列化。

适配器

TypeAdapter 最常用的一个作用就是类型转换,比如把 str 转换为 int。 这种场景下,写一个列表推导式也能搞定,但 TypeAdapter 更简洁一些。

from pydantic import TypeAdapter

int_adapter = TypeAdapter(list[int])
print(int_adapter.validate_json('[1, 2, 3]'))
# [1, 2, 3]

再复杂一点的,对嵌套数据进行类型转换。常用的一个场景是从 API 侧接收数据,或者从数据库中读取原始数据,然后转换为已经定义好的模型。

class Hobby(BaseModel):
    name: str

class User(BaseModel):
    name: str
    age: int
    hobbies: list[Hobby]

user_adapter = TypeAdapter(User)
print(user_adapter.validate_json('{"name": "Alice", "age": "25", "hobbies": [{"name": "reading"}, {"name": "traveling"}]}'))
# User(name='Alice', age=25, hobbies=[Hobby(name='reading'), Hobby(name='traveling')])

原始数据中的 age 虽然是 str 类型,但通过 TypeAdapter 会自动将 str 转换为 int

Pydantic 中有很多 API 功能是相似的,比如上面这种写法,本质上就跟 User.model_validate_json(raw)(以前叫 parse_raw) 效果是一样的,这两种都是 Pydantic 中 JSON 解析的方法。官方博客《Building a product search API with GPT-4 Vision, Pydantic, and FastAPI》中介绍了如何结合 JSON 解析及 LLM 构造一个产品搜索 API。

TypeAdapter v.s., model_validate_json

官方关于 json parsing 这一块并没有明确推荐用哪个,但有一些简单的建议,比如不要重复创建 TypeAdapter 实例,通常情况下,直接使用 model_validate_json(),而不是 model_validate(json.loads(...))

class SimpleUser(BaseModel):
    name: str
    age: int
class Hobby(BaseModel):
    name: str
class ComplexUser(BaseModel):
    name: str
    age: int
    hobbies: list[Hobby]

关于性能,我做了一个简单的测试(代码见:typeadapter-benchmark.py),对上面的 SimpleUserComplexUser 分别解析一百万次,结果如下:

  • 对于简单的模型解析,model_validate_json 比 TypeAdapter 快 2 倍左右。
  • 对于稍微复杂的模型解析,model_validate_json 比 TypeAdapter 快 1.6 倍左右。

从这个测试结果来看,model_validate_json 的性能比 TypeAdapter 好很多,如果项目中有大量 JSON 解析的需求,还是提前做一下性能测试,再决定使用哪个。

CaseTypeAdaptermodel_validate_jsonSpeedup
SimpleUser1.4027 (s)0.6437 (s)2.18x
ComplexUser2.1959 (s)1.3801 (s)1.60x

Why TypeAdapter?

即然 model_validate_json 比较快,为什么还要用 TypeAdapter 呢?

哎,使用 model_validate_json 的前提是我们有一个基于 BaseModel 的模型,如果项目中已经定义的模型不是 BaseModel,而是 dataclass,那就只能使用 TypeAdapter 了。

from dataclasses import dataclass
@dataclass
class User:
    name: str
    age: int

类型校验

既然 TypeAdapter 可以做类型转换,那类型校验自然也是理所当然的,只要转换不成功,那必然是类型不匹配。

class User(BaseModel):
    name: str
    age: int

user_adapter = TypeAdapter(User)
users=[{"name": "Alice", "age": "25"}, {"name": "Bob", "age": "30"}]
for user in users:
    try:
        print(user_adapter.validate_python(user))
    except ValidationError as e:
        print(e)

validate_json and validate_python

从上面的若干例子可以看出,TypeAdapter 中有 validate_jsonvalidate_python 两个方法,目的都是一样的,差异在于 validate_json 作用于 JSON 字符串及 bytes 类型,而 validate_python 作用于 Python 对象,比如 listdict

还有一种比较常见的解析方式是 Model(**data),这种其实也是调用 validate_python 进行解析的。

# BaseModel 的定义
def __init__(self, /, **data: Any) -> None:
    __tracebackhide__ = True
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
    if self is not validated_self:
        xxx

小结

对于类型转换及校验,TypeAdapter 提供了除 BaseModel 之外的另一种选择。 至于怎么选择,就看个人喜好了。

参考: