跳转至

Pydantic Company

我们根据我认为导致 Pydantic 成功的原则创办了一家公司。

公司公告中了解更多信息.

字段校验

自定义验证和对象之间的复杂关系可以使用 validator 装饰器来实现。

from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @validator('password2')
    def passwords_match(cls, v, values, **kwargs):
        if 'password1' in values and v != values['password1']:
            raise ValueError('passwords do not match')
        return v

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user)
#> name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'

try:
    UserModel(
        name='samuel',
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn2',
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for UserModel
    name
      must contain a space (type=value_error)
    password2
      passwords do not match (type=value_error)
    """

(这个脚本是完整的,它应该“按原样”运行)

关于验证器的一些注意事项:

  • 验证器是“类方法”,因此它们收到的第一个参数值是 UserModel 类,而不是UserModel的实例。
  • 第二个参数始终是要验证的字段值; 它可以随意命名
  • 您还可以将以下参数的任何子集添加到签名中(但名称必须匹配):
    • values: 包含任何先前验证字段的名称到值映射的字典
    • config: 模型配置
    • field: 正在验证的字段。 对象的类型是 pydantic.fields.ModelField
    • **kwargs: 如果提供,这将包括上面未在签名中明确列出的参数
  • 验证器应该返回解析后的值或引发 ValueErrorTypeErrorAssertionError(可以使用 assert 语句)。

Warning

如果您使用 assert 语句,请记住使用 -O 优化标志 运行 Python 会禁用 assert 语句,验证器将停止工作

  • 在验证器依赖于其他值的地方,你应该知道:

    • 验证时基于定义时的字段顺序. 例如。 在上面的示例中,password2 可以访问 password1(和 name),但是 password1 不能访问 password2。 有关字段如何排序的更多信息,请参阅 字段排序
    • 如果在另一个字段上验证失败(或该字段丢失),它将不会包含在values中,比如在本例中的 “if 'password1' in values and ...”。

前验证器和每项验证器(Pre and per-item validators)

验证器可以做一些更复杂的事情:

from typing import List
from pydantic import BaseModel, ValidationError, validator


class DemoModel(BaseModel):
    square_numbers: List[int] = []
    cube_numbers: List[int] = []

    # '*' is the same as 'cube_numbers', 'square_numbers' here:
    @validator('*', pre=True)
    def split_str(cls, v):
        if isinstance(v, str):
            return v.split('|')
        return v

    @validator('cube_numbers', 'square_numbers')
    def check_sum(cls, v):
        if sum(v) > 42:
            raise ValueError('sum of numbers greater than 42')
        return v

    @validator('square_numbers', each_item=True)
    def check_squares(cls, v):
        assert v ** 0.5 % 1 == 0, f'{v} is not a square number'
        return v

    @validator('cube_numbers', each_item=True)
    def check_cubes(cls, v):
        # 64 ** (1 / 3) == 3.9999999999999996 (!)
        # this is not a good way of checking cubes
        assert v ** (1 / 3) % 1 == 0, f'{v} is not a cubed number'
        return v


print(DemoModel(square_numbers=[1, 4, 9]))
#> square_numbers=[1, 4, 9] cube_numbers=[]
print(DemoModel(square_numbers='1|4|16'))
#> square_numbers=[1, 4, 16] cube_numbers=[]
print(DemoModel(square_numbers=[16], cube_numbers=[8, 27]))
#> square_numbers=[16] cube_numbers=[8, 27]
try:
    DemoModel(square_numbers=[1, 4, 2])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    square_numbers -> 2
      2 is not a square number (type=assertion_error)
    """

try:
    DemoModel(cube_numbers=[27, 27])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    cube_numbers
      sum of numbers greater than 42 (type=value_error)
    """
from pydantic import BaseModel, ValidationError, validator


class DemoModel(BaseModel):
    square_numbers: list[int] = []
    cube_numbers: list[int] = []

    # '*' is the same as 'cube_numbers', 'square_numbers' here:
    @validator('*', pre=True)
    def split_str(cls, v):
        if isinstance(v, str):
            return v.split('|')
        return v

    @validator('cube_numbers', 'square_numbers')
    def check_sum(cls, v):
        if sum(v) > 42:
            raise ValueError('sum of numbers greater than 42')
        return v

    @validator('square_numbers', each_item=True)
    def check_squares(cls, v):
        assert v ** 0.5 % 1 == 0, f'{v} is not a square number'
        return v

    @validator('cube_numbers', each_item=True)
    def check_cubes(cls, v):
        # 64 ** (1 / 3) == 3.9999999999999996 (!)
        # this is not a good way of checking cubes
        assert v ** (1 / 3) % 1 == 0, f'{v} is not a cubed number'
        return v


print(DemoModel(square_numbers=[1, 4, 9]))
#> square_numbers=[1, 4, 9] cube_numbers=[]
print(DemoModel(square_numbers='1|4|16'))
#> square_numbers=[1, 4, 16] cube_numbers=[]
print(DemoModel(square_numbers=[16], cube_numbers=[8, 27]))
#> square_numbers=[16] cube_numbers=[8, 27]
try:
    DemoModel(square_numbers=[1, 4, 2])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    square_numbers -> 2
      2 is not a square number (type=assertion_error)
    """

try:
    DemoModel(cube_numbers=[27, 27])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    cube_numbers
      sum of numbers greater than 42 (type=value_error)
    """

(这个脚本是完整的,它应该“按原样”运行)

还有几点需要注意:

  • 通过传递多个字段名称,单个验证器可以应用于多个字段
  • 通过传递特殊值“*”,也可以在所有字段上调用单个验证器
  • 关键字参数 pre 将导致验证器在其他验证之前被调用
  • 传递 each_item=True 将导致验证器应用于单个值(例如 ListDictSet 等),而不是整个对象

子类验证器和 each_item(Subclass Validators and each_item)

如果将验证器与引用父类上的List类型字段的子类一起使用,则使用each_item=True将导致验证器不运行; 相反,列表必须以编程方式迭代。

from typing import List
from pydantic import BaseModel, ValidationError, validator


class ParentModel(BaseModel):
    names: List[str]


class ChildModel(ParentModel):
    @validator('names', each_item=True)
    def check_names_not_empty(cls, v):
        assert v != '', 'Empty strings are not allowed.'
        return v


# This will NOT raise a ValidationError because the validator was not called
try:
    child = ChildModel(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
else:
    print('No ValidationError caught.')
    #> No ValidationError caught.


class ChildModel2(ParentModel):
    @validator('names')
    def check_names_not_empty(cls, v):
        for name in v:
            assert name != '', 'Empty strings are not allowed.'
        return v


try:
    child = ChildModel2(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
    """
    1 validation error for ChildModel2
    names
      Empty strings are not allowed. (type=assertion_error)
    """
from pydantic import BaseModel, ValidationError, validator


class ParentModel(BaseModel):
    names: list[str]


class ChildModel(ParentModel):
    @validator('names', each_item=True)
    def check_names_not_empty(cls, v):
        assert v != '', 'Empty strings are not allowed.'
        return v


# This will NOT raise a ValidationError because the validator was not called
try:
    child = ChildModel(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
else:
    print('No ValidationError caught.')
    #> No ValidationError caught.


class ChildModel2(ParentModel):
    @validator('names')
    def check_names_not_empty(cls, v):
        for name in v:
            assert name != '', 'Empty strings are not allowed.'
        return v


try:
    child = ChildModel2(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
    """
    1 validation error for ChildModel2
    names
      Empty strings are not allowed. (type=assertion_error)
    """

(这个脚本是完整的,它应该“按原样”运行)

始终验证(Validate Always)

出于性能原因,默认情况下,当未提供值时,不会为字段调用验证器。

然而,在某些情况下,始终调用验证器可能是有用的或需要的,例如 设置动态默认值。

from datetime import datetime

from pydantic import BaseModel, validator


class DemoModel(BaseModel):
    ts: datetime = None

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()


print(DemoModel())
#> ts=datetime.datetime(2023, 5, 8, 16, 38, 2, 661248)
print(DemoModel(ts='2017-11-08T14:00'))
#> ts=datetime.datetime(2017, 11, 8, 14, 0)

(这个脚本是完整的,它应该“按原样”运行)

您通常希望将它与 pre 一起使用,因为否则与 always=True pydantic 会尝试验证默认的 None,这会导致错误。

重用校验器(Reuse validators)

有时,您会希望在多个字段/模型上使用相同的验证器(例如,规范化一些输入数据)。 比较“naive”的写法是编写一个单独的函数,然后从多个装饰器中调用它。 显然,这需要大量重复和样板代码。 为了避免这种情况,allow_reuse 参数已添加到 v1.2 中的 pydantic.validator(默认情况下为 False):

from pydantic import BaseModel, validator


def normalize(name: str) -> str:
    return ' '.join((word.capitalize()) for word in name.split(' '))


class Producer(BaseModel):
    name: str

    # validators
    _normalize_name = validator('name', allow_reuse=True)(normalize)


class Consumer(BaseModel):
    name: str

    # validators
    _normalize_name = validator('name', allow_reuse=True)(normalize)


jane_doe = Producer(name='JaNe DOE')
john_doe = Consumer(name='joHN dOe')
assert jane_doe.name == 'Jane Doe'
assert john_doe.name == 'John Doe'

(这个脚本是完整的,它应该“按原样”运行)

很明显,重复已经减少,模型再次变得几乎是声明性的。

Tip

如果您有很多要验证的字段,定义一个帮助函数通常是有意义的,您可以使用它来避免一遍又一遍地设置 allow_reuse=True

根验证器(Root Validators)

还可以对整个模型的数据执行验证。

from pydantic import BaseModel, ValidationError, root_validator


class UserModel(BaseModel):
    username: str
    password1: str
    password2: str

    @root_validator(pre=True)
    def check_card_number_omitted(cls, values):
        assert 'card_number' not in values, 'card_number should not be included'
        return values

    @root_validator
    def check_passwords_match(cls, values):
        pw1, pw2 = values.get('password1'), values.get('password2')
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return values


print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn'))
#> username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
    UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      passwords do not match (type=value_error)
    """

try:
    UserModel(
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn',
        card_number='1234',
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      card_number should not be included (type=assertion_error)
    """

(这个脚本是完整的,它应该“按原样”运行)

如果 pre=True 根验证器引发错误,则不会进行字段验证。 与字段验证器一样,即使先前的验证器失败,默认情况下也会调用“post”(即 pre=False)根验证器; 可以通过为验证器设置 skip_on_failure=True 关键字参数来更改此行为。

values 参数将是一个字典,其中包含通过字段验证的值和适用的字段默认值。

字段检查(Field Checks)

在创建类时,会检查验证器以确认它们指定的字段确实存在于模型中。

然而,有时这是不可取的:例如 如果您定义一个验证器来验证继承模型上的字段。 在这种情况下,您应该在验证器上设置 check_fields=False

数据类验证器(Dataclass Validators)

验证器还可以与 pydantic 数据类一起使用。

from datetime import datetime

from pydantic import validator
from pydantic.dataclasses import dataclass


@dataclass
class DemoDataclass:
    ts: datetime = None

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()


print(DemoDataclass())
#> DemoDataclass(ts=datetime.datetime(2023, 5, 8, 16, 38, 2, 669536))
print(DemoDataclass(ts='2017-11-08T14:00'))
#> DemoDataclass(ts=datetime.datetime(2017, 11, 8, 14, 0))

(这个脚本是完整的,它应该“按原样”运行)