Перейти к содержанию

Декоратор валидации

??? API "Документация по API" pydantic.validate_call_decorator.validate_call

Декоратор @validate_call позволяет анализировать и проверять аргументы, передаваемые функции, с использованием аннотаций функции перед ее вызовом.

Несмотря на то, что внутри используется тот же подход к созданию и инициализации модели (более подробную информацию см. в разделе «Валидаторы »), он обеспечивает чрезвычайно простой способ применить проверку к вашему коду с минимальным использованием шаблона.

Пример использования:

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 . Все типы, перечисленные в типах, могут быть проверены, включая модели 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))

Несколько примечаний:

  • Хотя они передаются как строки, path и regex преобразуются декоратором в объект 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 для предоставления дополнительной информации о поле и проверках. Обычно его следует использовать в подсказке типа с Annotated , если не указан 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)

Использование с mypy

Декоратор validate_call должен работать с mypy «из коробки», поскольку он возвращает функцию с той же сигнатурой, что и функция, которую он декорирует. Единственное ограничение заключается в том, что, поскольку мы обманываем mypy, заставляя его думать, что функция, возвращаемая декоратором, такая же, как и декорируемая функция; доступ к необработанной функции или другим атрибутам потребует ввода type: ignore .

Необработанная функция

Необработанная функция, которая была декорирована, доступна. Это полезно, если в некоторых сценариях вы доверяете своим входным аргументам и хотите вызвать функцию наиболее производительным способом (см. примечания по производительности ниже):

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'

Асинхронные функции

@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://errors.pydantic.dev/2/v/greater_than',
            }
        ]
        """


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

Пользовательская конфигурация

Модель @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

Ограничения

Исключение проверки

В настоящее время при сбое проверки возникает стандартная ошибка Pydantic ValidationError. Подробности см. в разделе обработка ошибок модели .

Это полезно, поскольку его метод str() предоставляет полезную информацию о произошедшей ошибке, а такие методы, как .errors() и .json() могут быть полезны при раскрытии ошибок конечным пользователям. Однако ValidationError наследуется от ValueError , а не от TypeError , что может быть неожиданным, поскольку Python будет вызывать TypeError при недопустимых или отсутствующих аргументах. В будущем эту проблему можно решить, либо разрешив пользовательскую ошибку, либо вызвав другое исключение по умолчанию, либо и то, и другое.

Принуждение и строгость

Pydantic в настоящее время склоняется к попыткам привести типы, а не выдавать ошибку, если тип неправильный, см. Преобразование данных модели , и @validate_call ничем не отличается.

Производительность

Мы приложили большие усилия, чтобы сделать Pydantic максимально производительным, а проверка аргументов и создание модели выполняются только один раз, когда функция определена, однако использование декоратора @validate_call по-прежнему будет влиять на производительность по сравнению с вызовом необработанной функции. .

Во многих ситуациях это будет иметь незначительный или вообще не иметь заметного эффекта, однако имейте в виду, что @validate_call не является эквивалентом или альтернативой определениям функций в строго типизированных языках; этого никогда не будет.

Возвращаемое значение

Возвращаемое значение функции может быть дополнительно проверено на соответствие ее аннотации типа возвращаемого значения.


本文总阅读量