跳转至

验证装饰器

API 文档

pydantic.validate_call_decorator.validate_call

@validate_call 装饰器允许在调用函数之前,使用函数的注释来解析和验证传递给函数的参数。

虽然在幕后它使用了与模型创建和初始化相同的方法(更多详细信息请参见验证器),但它提供了一种极其简单的方法,可以在代码中应用验证,而无需大量样板代码。

Example of usage:

from pydantic import ValidationError, validate_call


@validate_call
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat('x', '4', separator=b' ')
print(b)
#> b'x x x x'

try:
    c = repeat('hello', 'wrong')
except ValidationError as exc:
    print(exc)
    """
    1 validation error for repeat
    1
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='wrong', input_type=str]
    """

参数类型

参数类型是根据函数上的类型注释推断出来的,没有类型装饰器的参数被视为 Any 。 types 中列出的所有类型都可以进行验证,包括 Pydantic 模型和自定义类型。与 Pydantic 的其他部分一样,类型可以在传递给实际函数之前被装饰器强制转换:

# TODO replace find_file with something that isn't affected the filesystem
import os
from pathlib import Path
from typing import Optional, Pattern

from pydantic import DirectoryPath, validate_call


@validate_call
def find_file(path: DirectoryPath, regex: Pattern, max=None) -> Optional[Path]:
    for i, f in enumerate(path.glob('**/*')):
        if max and i > max:
            return
        if f.is_file() and regex.fullmatch(str(f.relative_to(path))):
            return f


# note: this_dir is a string here
this_dir = os.path.dirname(__file__)

print(find_file(this_dir, '^validation.*'))
print(find_file(this_dir, '^foobar.*', max=3))

A few notes:

  • 尽管它们被传递为字符串,但 pathregex 会被装饰器转换为 Path 对象和正则表达式。

  • max 没有类型注释,因此将被装饰器视为 Any

这种类型强制转换可能非常有用,但也可能令人困惑或不希望这样做。有关 @validate_call 在这方面的局限性的讨论,请参见强制转换和严格性。

函数签名

@validate_call 装饰器旨在与使用所有可能的参数配置和这些参数的所有可能组合的函数一起使用:

  • 位置参数或关键字参数,带或不带默认值。

  • 通过 * (通常为 *args )定义的可变位置参数。

  • 通过 ** (通常为 **kwargs )定义的可变关键字参数。

  • 仅关键字参数: *, 后的参数。

  • 仅限位置参数: , / 之前的参数(Python 3.8 中的新功能)。

为了演示所有上述参数类型:

from pydantic import validate_call


@validate_call
def pos_or_kw(a: int, b: int = 2) -> str:
    return f'a={a} b={b}'


print(pos_or_kw(1))
#> a=1 b=2
print(pos_or_kw(a=1))
#> a=1 b=2
print(pos_or_kw(1, 3))
#> a=1 b=3
print(pos_or_kw(a=1, b=3))
#> a=1 b=3


@validate_call
def kw_only(*, a: int, b: int = 2) -> str:
    return f'a={a} b={b}'


print(kw_only(a=1))
#> a=1 b=2
print(kw_only(a=1, b=3))
#> a=1 b=3


@validate_call
def pos_only(a: int, b: int = 2, /) -> str:  # python 3.8 only
    return f'a={a} b={b}'


print(pos_only(1))
#> a=1 b=2
print(pos_only(1, 2))
#> a=1 b=2


@validate_call
def var_args(*args: int) -> str:
    return str(args)


print(var_args(1))
#> (1,)
print(var_args(1, 2))
#> (1, 2)
print(var_args(1, 2, 3))
#> (1, 2, 3)


@validate_call
def var_kwargs(**kwargs: int) -> str:
    return str(kwargs)


print(var_kwargs(a=1))
#> {'a': 1}
print(var_kwargs(a=1, b=2))
#> {'a': 1, 'b': 2}


@validate_call
def armageddon(
    a: int,
    /,  # python 3.8 only
    b: int,
    *c: int,
    d: int,
    e: int = None,
    **f: int,
) -> str:
    return f'a={a} b={b} c={c} d={d} e={e} f={f}'


print(armageddon(1, 2, d=3))
#> a=1 b=2 c=() d=3 e=None f={}
print(armageddon(1, 2, 3, 4, 5, 6, d=8, e=9, f=10, spam=11))
#> a=1 b=2 c=(3, 4, 5, 6) d=8 e=9 f={'f': 10, 'spam': 11}

使用字段描述函数参数

字段也可以与 @validate_call 一起使用,以提供有关该字段和验证的其他信息。通常,它应该在带有注释的类型提示中使用,除非指定了 default_factory ,否则在这种情况下,它应该用作字段的默认值:

from datetime import datetime

from typing_extensions import Annotated

from pydantic import Field, ValidationError, validate_call


@validate_call
def how_many(num: Annotated[int, Field(gt=10)]):
    return num


try:
    how_many(1)
except ValidationError as e:
    print(e)
    """
    1 validation error for how_many
    0
      Input should be greater than 10 [type=greater_than, input_value=1, input_type=int]
    """


@validate_call
def when(dt: datetime = Field(default_factory=datetime.now)):
    return dt


print(type(when()))
#> <class 'datetime.datetime'>

alias 可以像普通装饰器一样使用。

from typing_extensions import Annotated

from pydantic import Field, validate_call


@validate_call
def how_many(num: Annotated[int, Field(gt=10, alias='number')]):
    return num


how_many(number=42)

Usage with mypy

validate_call 装饰器应该可以与 mypy 一起“开箱即用”,因为它被定义为返回一个具有与它装饰的函数相同签名的函数。唯一的限制是,由于我们欺骗 mypy 认为装饰器返回的函数与被装饰的函数相同;因此,访问原始函数或其他属性将需要 type: ignore

Raw function

被修饰的原始函数是可访问的,如果在某些情况下您信任输入参数并希望以最高性能的方式调用该函数(请参阅下面关于性能的注释),这将很有用:

from pydantic import validate_call


@validate_call
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat.raw_function('good bye', 2, separator=b', ')
print(b)
#> b'good bye, good bye'

Async functions

@validate_call 也可以用于异步函数:

class Connection:
    async def execute(self, sql, *args):
        return 'testing@example.com'


conn = Connection()
# ignore-above
import asyncio

from pydantic import PositiveInt, ValidationError, validate_call


@validate_call
async def get_user_email(user_id: PositiveInt):
    # `conn` is some fictional connection to a database
    email = await conn.execute('select email from users where id=$1', user_id)
    if email is None:
        raise RuntimeError('user not found')
    else:
        return email


async def main():
    email = await get_user_email(123)
    print(email)
    #> testing@example.com
    try:
        await get_user_email(-4)
    except ValidationError as exc:
        print(exc.errors())
        """
        [
            {
                'type': 'greater_than',
                'loc': (0,),
                'msg': 'Input should be greater than 0',
                'input': -4,
                'ctx': {'gt': 0},
                'url': 'https://pydantic.com.cn/errors/validation_errors#greater_than',
            }
        ]
        """


asyncio.run(main())
# requires: `conn.execute()` that will return `'testing@example.com'`

Custom config

@validate_call 背后的模型可以使用 config 设置进行自定义,这相当于在普通模型中设置 ConfigDict 子类。

使用装饰器的 config 关键字参数进行配置设置,它可以是一个配置类,也可以是一个属性字典,稍后将其转换为类。

from pydantic import ValidationError, validate_call


class Foobar:
    def __init__(self, v: str):
        self.v = v

    def __add__(self, other: 'Foobar') -> str:
        return f'{self} + {other}'

    def __str__(self) -> str:
        return f'Foobar({self.v})'


@validate_call(config=dict(arbitrary_types_allowed=True))
def add_foobars(a: Foobar, b: Foobar):
    return a + b


c = add_foobars(Foobar('a'), Foobar('b'))
print(c)
#> Foobar(a) + Foobar(b)

try:
    add_foobars(1, 2)
except ValidationError as e:
    print(e)
    """
    2 validation errors for add_foobars
    0
      Input should be an instance of Foobar [type=is_instance_of, input_value=1, input_type=int]
    1
      Input should be an instance of Foobar [type=is_instance_of, input_value=2, input_type=int]
    """

扩展——在调用函数之前验证参数

在某些情况下,将函数参数的验证与函数调用本身分开可能会有所帮助。当特定函数成本高/耗时时,这可能很有用。

以下是针对该模式可以使用的一种解决方法示例:

from pydantic import validate_call


@validate_call
def validate_foo(a: int, b: int):
    def foo():
        return a + b

    return foo


foo = validate_foo(a=1, b=2)
print(foo())
#> 3

Limitations

验证异常

当前,在验证失败时,会引发标准的 Pydantic ValidationError 。有关详细信息,请参见模型错误处理。

这很有帮助,因为它的 str() 方法提供了有关发生错误的有用详细信息,并且像 .errors().json() 这样的方法在向最终用户公开错误时可能很有用。但是, ValidationErrorValueError 继承,而不是 TypeError ,这可能是出乎意料的,因为 Python 会在出现无效或缺少参数时引发 TypeError 。这可能会在未来通过允许自定义错误或默认情况下引发不同的异常来解决,或者两者兼而有之。

强制和严格

Pydantic 当前倾向于尝试强制类型,如果类型错误则不引发错误,见模型数据转换和 @validate_call 没有什么不同。

Performance

我们已经付出了很大的努力,使 Pydantic 尽可能地高效,并且参数检查和模型创建只在函数定义时执行一次,但是与直接调用原始函数相比,使用 @validate_call 装饰器仍然会有性能影响。

在许多情况下,这几乎没有或没有明显的效果,但是请注意, @validate_call 不是强类型语言中函数定义的等效或替代;它永远不会是。

返回值

函数的返回值可以根据其返回类型注解进行可选的验证。


本文总阅读量