Pydantic Field 字段有什么用
- Authors
- @SLIPPERTOPIA
日常开发中,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)
指定默认值的方法有两种:default
和 default_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 提供了别名机制,即除了字段名之外,还可以指定其他字段名。别名机制主要有四个参数:alias
、alias_priority
、serialization_alias
、validation_alias
。主要作用分别如下 :
参数 | 主要作用 |
---|---|
alias | 通用别名设置,也是最常用的参数 |
alias_priority | 控制是否优先使用别名生成器 |
serialization_alias | 指定字段的序列化别名,主要用于序列化时,使用别名进行转换 |
validation_alias | 指定字段的验证别名,主要用于数据验证时,使用别名进行转换 |
alias
通过 alias
设置字段的别名,主要使用场景有:
- 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}
- 处理 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'})
- 处理不同命名风格: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]
,不同值的意思:
2
,不使用别名生成器,使用字段名。1
,使用别名生成器,如果未定义别名生成器,则使用字段名。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
本身提供了一些参数用于验证输入合法性,比如 ge
、le
、min_length
、min_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 开始,再或者用户名不能包含非英文字符。基础类型验证无法满足这些需求,但结合 Field
和 field_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 约束
constr
是 Annotated
的简化版,是 Pydantic 内置的对字符串的约束,对应的还有 conint
、confloat
、conlist
、conset
、condict
等。
from pydantic import constr
class User(BaseModel):
username: constr(min_length=5, max_length=30, pattern=r'^[a-zA-Z]+$')
注:从 Pydantic 3.0 开始,不再推荐使用 constr
,而是推荐使用 Annotated
和 StringConstraints
,即如下写法:
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 是真的简洁,还是得多看文档,多实践。