Validierungs-Dekorator
??? API „API-Dokumentation“ pydantic.validate_call_decorator.validate_call
Mit dem Dekorator @validate_call
können die an eine Funktion übergebenen Argumente mithilfe der Anmerkungen der Funktion analysiert und validiert werden, bevor die Funktion aufgerufen wird.
Obwohl dabei unter der Haube derselbe Ansatz der Modellerstellung und -initialisierung verwendet wird (weitere Einzelheiten finden Sie unter Validatoren ), bietet es eine äußerst einfache Möglichkeit, mit minimalem Boilerplate eine Validierung auf Ihren Code anzuwenden.
Anwendungsbeispiel:
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]
"""
Argumenttypen¶
Argumenttypen werden aus Typanmerkungen der Funktion abgeleitet, Argumente ohne Typdekorator werden als Any
betrachtet. Alle in Typen aufgeführten Typen können validiert werden, einschließlich Pydantic-Modellen und benutzerdefinierten Typen . Wie beim Rest von Pydantic können Typen vom Dekorateur erzwungen werden, bevor sie an die eigentliche Funktion übergeben werden:
# 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))
Ein paar Anmerkungen:
- Obwohl sie als Zeichenfolgen übergeben werden, werden
path
undregex
vom Dekorateur in einPath
-Objekt bzw. einen Regex konvertiert. max
hat keine Typanmerkung und wird daher vom Dekorateur alsAny
betrachtet.
Ein solcher Typzwang kann äußerst hilfreich, aber auch verwirrend oder unerwünscht sein. Eine Diskussion der diesbezüglichen Einschränkungen von @validate_call
finden Sie unter Zwang und Strenge .
Funktionssignaturen¶
Der @validate_call
Dekorator ist so konzipiert, dass er mit Funktionen arbeitet, die alle möglichen Parameterkonfigurationen und alle möglichen Kombinationen davon verwenden:
- Positions- oder Schlüsselwortargumente mit oder ohne Standardwerte.
- Variable Positionsargumente, definiert über
*
(häufig*args
). - Variable Schlüsselwortargumente, die über
**
(häufig**kwargs
) definiert werden. - Nur-Schlüsselwort-Argumente: Argumente nach
*,
. - Nur positionelle Argumente: Argumente vor
, /
(neu in Python 3.8).
Um alle oben genannten Parametertypen zu demonstrieren:
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}
Verwenden von Field zum Beschreiben von Funktionsargumenten¶
Field kann auch mit @validate_call
verwendet werden, um zusätzliche Informationen über das Feld und Validierungen bereitzustellen. Im Allgemeinen sollte es in einem Typhinweis mit Annotated verwendet werden, es sei denn, default_factory
ist angegeben. In diesem Fall sollte es als Standardwert des Felds verwendet werden:
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'>
Der alias
kann wie gewohnt mit dem Decorator verwendet werden.
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)
Verwendung mit mypy¶
Der validate_call
Dekorator sollte sofort mit mypy funktionieren, da er so definiert ist, dass er eine Funktion mit derselben Signatur zurückgibt wie die Funktion, die er dekoriert. Die einzige Einschränkung besteht darin, dass wir mypy dazu verleiten, zu glauben, dass die vom Dekorateur zurückgegebene Funktion dieselbe ist wie die Funktion, die dekoriert wird. Für den Zugriff auf die Rohfunktion oder andere Attribute ist type: ignore
erforderlich.
Rohfunktion¶
Auf die dekorierte Rohfunktion kann zugegriffen werden. Dies ist nützlich, wenn Sie in einigen Szenarien Ihren Eingabeargumenten vertrauen und die Funktion auf die leistungsfähigste Weise aufrufen möchten (siehe Hinweise zur Leistung unten):
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'
Asynchrone Funktionen¶
@validate_call
kann auch für asynchrone Funktionen verwendet werden:
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'`
Benutzerdefinierte Konfiguration¶
Das Modell hinter @validate_call
kann mithilfe einer config
angepasst werden, was dem Festlegen der Unterklasse ConfigDict
in normalen Modellen entspricht.
Die Konfiguration wird mithilfe des Schlüsselwortarguments config
für den Dekorator festgelegt. Dabei kann es sich entweder um eine Konfigurationsklasse oder um ein Diktat von Eigenschaften handeln, die später in eine Klasse konvertiert werden.
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]
"""
Erweiterung – Argumente validieren, bevor eine Funktion aufgerufen wird¶
In manchen Fällen kann es hilfreich sein, die Validierung der Argumente einer Funktion vom Funktionsaufruf selbst zu trennen. Dies kann nützlich sein, wenn eine bestimmte Funktion kostspielig/zeitaufwändig ist.
Hier ist ein Beispiel für eine Problemumgehung, die Sie für dieses Muster verwenden können:
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
Einschränkungen¶
Validierungsausnahme¶
Derzeit wird bei einem Validierungsfehler ein standardmäßiger Pydantic ValidationError
ausgelöst. Einzelheiten finden Sie unter Modellfehlerbehandlung .
Dies ist hilfreich, da die Methode str()
nützliche Details zum aufgetretenen Fehler bereitstellt und Methoden wie .errors()
und .json()
hilfreich sein können, wenn die Fehler Endbenutzern angezeigt werden. Allerdings erbt ValidationError
von ValueError
und nicht von TypeError
, was unerwartet sein kann, da Python bei ungültigen oder fehlenden Argumenten einen TypeError
auslösen würde. Dies kann in Zukunft möglicherweise dadurch behoben werden, dass entweder ein benutzerdefinierter Fehler zugelassen wird oder standardmäßig eine andere Ausnahme ausgelöst wird, oder beides.
Zwang und Strenge¶
Pydantic tendiert derzeit dazu, Typen zu erzwingen, anstatt einen Fehler auszulösen, wenn ein Typ falsch ist. Siehe Modelldatenkonvertierung , und @validate_call
ist nicht anders.
Leistung¶
Wir haben große Anstrengungen unternommen, um Pydantic so leistungsfähig wie möglich zu machen, und die Argumentprüfung und Modellerstellung wird nur einmal durchgeführt, wenn die Funktion definiert ist. Allerdings wird die Verwendung des @validate_call
Dekorators im Vergleich zum Aufruf der Rohfunktion immer noch Auswirkungen auf die Leistung haben .
In vielen Situationen hat dies nur geringe oder keine spürbare Auswirkung. Beachten Sie jedoch, dass @validate_call
kein Äquivalent oder keine Alternative zu Funktionsdefinitionen in stark typisierten Sprachen ist. das wird es nie sein.
Rückgabewert¶
Der Rückgabewert der Funktion kann optional anhand seiner Rückgabetypanmerkung validiert werden.
本文总阅读量次