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
etregex
sont respectivement convertis en objetPath
et regex par le décorateur. max
n'a pas d'annotation de type, il sera donc considéré commeAny
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.
本文总阅读量次