WheatField
WheatField

Pydantic Field 字段有什么用

November 13, 20241976 words, 10 min read
Authors

日常开发中,Field 应该是除 BaseModel 之外,代码中最常见的 Pydantic 关键字了。 除了指定类型之外, Field 还支持很多功能,函数声明(为节省篇幅,省略了部分参数)中的参数多达 20 多个,但就我日常开发体验来看,常用的也就别名、验证输入、默认值这些概念,下面就这几点展开聊一下。更详细的官方文档列的更全,这里不再赘述。

Field(
    default: Any = PydanticUndefined,
    *,
    default_factory: (
        Callable[[], Any]
        | Callable[[dict[str, Any]], Any]
        | None
    ) = _Unset,
    alias: str | None = _Unset,
    alias_priority: int | None = _Unset,
    validation_alias: (
        str | AliasPath | AliasChoices | None
    ) = _Unset,
    serialization_alias: str | None = _Unset,
    ...
) -> Any

默认值(default 和 default_factory)

指定默认值的方法有两种:defaultdefault_factory

  • default 直接指定字段的默认值,比如 0,"hello",[], 等。
  • default_factory 通过工厂函数生成默认值,当需要复杂默认值或延迟初始化时使用。
def user_name_factory():
    return "John" + str(random.randint(0, 100))

class User(BaseModel):
    name: str = Field(default="John")
    username: str = Field(default_factory=user_name_factory)

在 Pydantic 中,不论是工厂函数还是后面提到的验证函数,最好都不要长时间占用 IO,比如数据库连接、文件读取等,这些操作不应该在函数内部进行。 虽然 fastAPI 支持异步接口,但 Pydantic 的验证函数本身并不支持 async。 因此 Pydantic 的类型验证、序列化尽量是 IO 无关的,一句话来说也就是:拿到数据后再 Pydantic

别名

为了兼容 API 及不同命名风格,Pydantic 提供了别名机制,即除了字段名之外,还可以指定其他字段名。别名机制主要有四个参数:aliasalias_priorityserialization_aliasvalidation_alias。主要作用分别如下 :

参数主要作用
alias通用别名设置,也是最常用的参数
alias_priority控制是否优先使用别名生成器
serialization_alias指定字段的序列化别名,主要用于序列化时,使用别名进行转换
validation_alias指定字段的验证别名,主要用于数据验证时,使用别名进行转换

alias

通过 alias 设置字段的别名,主要使用场景有:

  1. API 字段名转换:e.g., 从 API 返回的字段名和内部字段名不同,需要重新命名。
class User(BaseModel):
    # API 返回 'user_name',但代码里用 'name'
    name: str = Field(alias='user_name')
    # API 返回 'userAge',但代码里用 'age'
    age: int = Field(alias='userAge')

# 使用示例
user = User(user_name="张三", userAge=25)
print(user.name)  # 输出: 张三
print(user.age)   # 输出: 25
print(user.model_dump(by_alias=True))  # {'user_name': '张三', 'userAge': 25}
  1. 处理 JSON 中的特殊字符:e.g., JSON 中是 $schema,但 Python 变量名不能用 $,或者 JSON 中是 @type,但 Python 变量名不能用 @
class Config(BaseModel):
    schema: str = Field(alias='$schema')
    type: str = Field(alias='@type')
config = Config(**{'$schema': 'http://...', '@type': 'object'})
  1. 处理不同命名风格:e.g., 使用 Python 对接 JS 返回数据时,JS 返回字段名是驼峰,而 Python 变量名用下划线。
class Product(BaseModel):
    # API 用驼峰,代码用下划线
    product_name: str = Field(alias='productName')
    product_price: float = Field(alias='productPrice')
    created_at: datetime = Field(alias='createdAt')

注:在序列化时需要使用 by_alias=True 来指定使用别名,不然会使用字段名。

alias_priority

再说一下 alias_priority,这个参数主要控制别名生成器的优先级,平时很少用到,但需要用到的时候,确实很方便。这个参数有三个值 [2, 1, None],不同值的意思:

  1. 2,不使用别名生成器,使用字段名。
  2. 1,使用别名生成器,如果未定义别名生成器,则使用字段名。
  3. None,如果定义了 alias,则使用 alias,不然使用别名生成器。

别名生成器实际上就是一个函数,对输入数据进行转换,生成一个新的数据。 举个例子,后端写手多以 Go, Java 语言为主,API 的命名风格通常是驼峰式 🐪,而算法同学多以 Python 为主,命名风格多以小写加下划线为主。由于历史遗留原因,从后端返回的字段也都是驼峰格式的,但算法新项目中倾向于用 Pythonic 的方式去开发,如果需要使用 Pydantic 进行数据验证,则需要对每个字段都声明一个别名(e.g., user_name -> UserName),非常麻烦。 这时候就可以通过设置别名生成器来批量完成。

from pydantic import BaseModel, ConfigDict, Field

def to_camel(string: str) -> str:
    return ''.join(word.capitalize() for word in string.split('_'))

class Voice(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel)

    user_name: str
    language_code: str = Field(alias='lang', alias_priority=2)


voice = Voice(**{"UserName": "Filiz", "lang": "tr-TR"})
print(voice.language_code)
# > tr-TR
print(voice.model_dump(by_alias=True))
# > {'Name': 'Filiz', 'lang': 'tr-TR'}

注意:关于这个参数,一定不要轻信 GPT、Claude 之类大模型的解释,它们会告诉你这个参数是解决别名冲突的,但根本不是这回事。

serialization_alias

serialization_alias 指定字段的序列化别名,主要用于在序列化时,使用自定义别名进行转换,不会影响字段名及别名(alias)。 以下面代码为例,序列化时,如果 by_alias=True,输出的序列化别名是 user_name,使用 by_alias=False 时,输出的是普通别名 UserName。 如果没有别名,则输出字段名 name

class User(BaseModel):
    name: str = Field(alias='UserName', serialization_alias='user_name')

user = User(**{"UserName": "Filiz"})
print(user.name)
# > Filiz
print(user.model_dump(by_alias=True))
# > {'user_name': 'Filiz'}
print(user.model_dump(by_alias=False))
# > {'UserName': 'Filiz'}

validation_alias

validation_alias 用于在数据验证时将输入字段名映射到模型字段名,主要用于处理输入数据的字段名与模型字段名不一致的情况。 结合上面的 serialization_alias 可以将输出限定为 UserName,输出为 user_name。

class User(BaseModel):
    username: str = Field(validation_alias='UserName', serialization_alias='user_name')

user = User(UserName='Filiz')
print(user.username)
# > Filiz

验证输入数据

前面提到是以什么名称输入数据的问题,那如何验证输入的数据呢? 比如判断输出的年龄是否在 0 ~ 150 之间,用户输出的手机号是否以 1 开头且总计 11 位数字,再或者判断 username 的长度是否在 5 ~ 30 之间,并且只能包含字母。

基本类型验证

对于基础类型,比如整数、浮点数、字符串、列表、字典等,Field 本身提供了一些参数用于验证输入合法性,比如 gelemin_lengthmin_items 等。以年龄输入为例,Field(ge=0, le=150) 表示年龄必须大于等于0,小于等于150。

from pydantic import BaseModel, Field

class User(BaseModel):
    # 基本类型验证
    age: int = Field(ge=0, le=150)  # 大于等于0,小于等于150
    name: str = Field(min_length=2, max_length=50)
    score: float = Field(ge=0.0, le=100.0)
    pets: dict[str, str] = Field(min_items=1, max_items=3)

复杂验证

一些场景下,需要对字段输入进行更为复杂的验证,比如用户名不能包含敏感词,手机号不能从 0 开始,再或者用户名不能包含非英文字符。基础类型验证无法满足这些需求,但结合 Fieldfield_validator 还是可以轻松实现的。

还以上面提到的用户名为例,长度在 5 ~ 30 之间,并且只能包含字母。

直接使用 Field 验证

class User(BaseModel):
    username: str = Field(min_length=5, max_length=30, pattern=r'^[a-zA-Z]+$')

使用验证器 field_validator

之前使用 @validator 装饰器,现在推荐 @field_validator

from pydantic import field_validator

class User(BaseModel):
    username: str

    @field_validator('username')
    def validate_username(cls, v):
        if len(v) < 5 or len(v) > 30 or not v.isalpha():
            raise ValueError('Username must be 5-30 characters long and contain only letters')
        return v

使用 constr 约束

constrAnnotated 的简化版,是 Pydantic 内置的对字符串的约束,对应的还有 conintconfloatconlistconsetcondict 等。

from pydantic import constr

class User(BaseModel):
    username: constr(min_length=5, max_length=30, pattern=r'^[a-zA-Z]+$')

注:从 Pydantic 3.0 开始,不再推荐使用 constr,而是推荐使用 AnnotatedStringConstraints,即如下写法:

from pydantic import BaseModel, Field, StringConstraints,Annotated
class User(BaseModel):
    username: str = Annotated[
        str,
        StringConstraints(min_length=5, max_length=30, pattern=r'^[a-zA-Z]+$')
    ]

小结

Pydantic 的类型验证、别名功能还是很强大的,之前不太熟悉,在一些项目里又生造了很多轮子,即不优雅又不通用。熟悉之后才发现 Pydantic 是真的简洁,还是得多看文档,多实践。