Aller au contenu

Décorateur de validation

??? API "Documentation API" pydantic.validate_call_decorator.validate_call

Le décorateur @validate_call permet aux arguments passés à une fonction d'être analysés et validés à l'aide des annotations de la fonction avant que la fonction ne soit appelée.

Bien que sous le capot, cela utilise la même approche de création et d'initialisation de modèle (voir Validateurs pour plus de détails), il fournit un moyen extrêmement simple d'appliquer la validation à votre code avec un minimum de passe-partout.

Exemple d'utilisation :

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]
    """

Types d'arguments

Les types d'arguments sont déduits des annotations de type sur la fonction, les arguments sans décorateur de type sont considérés comme Any . Tous les types répertoriés dans types peuvent être validés, y compris les modèles Pydantic et les types personnalisés . Comme pour le reste de Pydantic, les types peuvent être contraints par le décorateur avant d'être transmis à la fonction réelle:

# 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))

Quelques remarques :

  • Bien qu'ils soient transmis sous forme de chaînes, path et regex sont respectivement convertis en objet Path et regex par le décorateur.
  • max n'a pas d'annotation de type, il sera donc considéré comme Any par le décorateur.

Une coercition de type comme celle-ci peut être extrêmement utile, mais aussi déroutante ou non souhaitée. Voir Coercition et rigueur pour une discussion sur les limites de @validate_call à cet égard.

Signatures de fonction

Le décorateur @validate_call est conçu pour fonctionner avec des fonctions utilisant toutes les configurations de paramètres possibles et toutes les combinaisons possibles de celles-ci:

  • Arguments de position ou de mot-clé avec ou sans valeurs par défaut.
  • Arguments de position variables définis via * (souvent *args ).
  • Arguments de mots-clés variables définis via ** (souvent **kwargs ).
  • Arguments de mots-clés uniquement: arguments après *, .
  • Arguments de position uniquement: arguments avant , / (nouveau dans Python 3.8).

Pour démontrer tous les types de paramètres ci-dessus:

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}

Utiliser Field pour décrire les arguments de la fonction

Le champ peut également être utilisé avec @validate_call pour fournir des informations supplémentaires sur le champ et les validations. En général, il doit être utilisé dans un indice de type avec Annotated , sauf si default_factory est spécifié, auquel cas il doit être utilisé comme valeur par défaut du champ:

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'>

L’ alias peut être utilisé normalement avec le décorateur.

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)

Utilisation avec mypy

Le décorateur validate_call devrait fonctionner "prêt à l'emploi" avec mypy puisqu'il est défini pour renvoyer une fonction avec la même signature que la fonction qu'il décore. La seule limitation est que puisque nous trompons mypy en lui faisant croire que la fonction renvoyée par le décorateur est la même que la fonction en cours de décoration; l'accès à la fonction brute ou à d'autres attributs nécessitera type: ignore .

Fonction brute

La fonction brute qui a été décorée est accessible, ceci est utile si dans certains scénarios vous faites confiance à vos arguments d'entrée et souhaitez appeler la fonction de la manière la plus performante (voir les notes sur les performances ci-dessous):

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'

Fonctions asynchrones

@validate_call peut également être utilisé sur les fonctions asynchrones:

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'`

Configuration personnalisée

Le modèle derrière @validate_call peut être personnalisé à l'aide d'un paramètre config , ce qui équivaut à définir la sous-classe ConfigDict dans les modèles normaux.

La configuration est définie à l'aide de l'argument de mot-clé config du décorateur, il peut s'agir soit d'une classe de configuration, soit d'un dict de propriétés qui sont converties ultérieurement en classe.

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]
    """

Extension - valider les arguments avant d'appeler une fonction

Dans certains cas, il peut être utile de séparer la validation des arguments d'une fonction de l'appel de fonction lui-même. Cela peut être utile lorsqu'une fonction particulière est coûteuse/prend du temps.

Voici un exemple de solution de contournement que vous pouvez utiliser pour ce modèle:

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

Limites

Exception de validation

Actuellement, en cas d'échec de la validation, une erreur Pydantic standard ValidationError est générée. Voir la gestion des erreurs du modèle pour plus de détails.

Ceci est utile car sa méthode str() fournit des détails utiles sur l'erreur qui s'est produite et des méthodes telles que .errors() et .json() peuvent être utiles pour exposer les erreurs aux utilisateurs finaux. Cependant, ValidationError hérite de ValueError et non TypeError , ce qui peut être inattendu puisque Python déclencherait une TypeError en cas d'arguments invalides ou manquants. Ce problème pourra être résolu à l'avenir en autorisant une erreur personnalisée ou en déclenchant une exception différente par défaut, ou les deux.

Coercition et rigueur

Pydantic penche actuellement du côté d'essayer de contraindre les types plutôt que de générer une erreur si un type est erroné, voir la conversion des données du modèle et @validate_call n'est pas différent.

Performance

Nous avons fait de gros efforts pour rendre Pydantic aussi performant que possible et l'inspection des arguments et la création du modèle ne sont effectuées qu'une seule fois lorsque la fonction est définie, mais l'utilisation du décorateur @validate_call aura toujours un impact sur les performances par rapport à l'appel de la fonction brute. .

Dans de nombreuses situations, cela aura peu ou pas d'effet notable, mais sachez que @validate_call n'est pas un équivalent ou une alternative aux définitions de fonctions dans les langages fortement typés ; cela ne le sera jamais.

Valeur de retour

La valeur de retour de la fonction peut éventuellement être validée par rapport à son annotation de type de retour.


本文总阅读量