Ga naar inhoud

Validatie-decorator

??? api "API-documentatie" pydantic.validate_call_decorator.validate_call

Met de decorator @validate_call kunnen de argumenten die aan een functie worden doorgegeven, worden geparseerd en gevalideerd met behulp van de annotaties van de functie voordat de functie wordt aangeroepen.

Hoewel dit onder de motorkap dezelfde aanpak voor het maken en initialiseren van modellen gebruikt (zie Validators voor meer details), biedt het een uiterst eenvoudige manier om validatie op uw code toe te passen met minimale standaardregels.

Voorbeeld van gebruik:

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 worden afgeleid uit typeannotaties op de functie, argumenten zonder type-decorator worden beschouwd als Any . Alle typen vermeld in typen kunnen worden gevalideerd, inclusief Pydantic-modellen en aangepaste typen . Net als bij de rest van Pydantic kunnen typen door de binnenhuisarchitect worden gedwongen voordat ze worden doorgegeven aan de daadwerkelijke functie:

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

Een paar opmerkingen:

  • Hoewel ze als tekenreeksen worden doorgegeven, worden path en regex door de decorateur geconverteerd naar respectievelijk een Path object en een regex.
  • max heeft geen typeannotatie, dus wordt door de binnenhuisarchitect als Any beschouwd.

Dit soort dwang kan uiterst nuttig zijn, maar ook verwarrend of ongewenst. Zie Dwang en strengheid voor een bespreking van de beperkingen van @validate_call in dit opzicht.

Functie handtekeningen

De @validate_call decorateur is ontworpen om te werken met functies die alle mogelijke parameterconfiguraties en alle mogelijke combinaties hiervan gebruiken:

  • Positionele of trefwoordargumenten met of zonder standaardwaarden.
  • Variabele positionele argumenten gedefinieerd via * (vaak *args ).
  • Variabele trefwoordargumenten gedefinieerd via ** (vaak **kwargs ).
  • Argumenten met alleen trefwoorden: argumenten na *, .
  • Alleen-positionele argumenten: argumenten vóór , / (nieuw in Python 3.8).

Om alle bovenstaande parametertypen te demonstreren:

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}

Veld gebruiken om functieargumenten te beschrijven

Veld kan ook worden gebruikt met @validate_call om extra informatie over het veld en validaties te geven. Over het algemeen moet het worden gebruikt in een type hint met Annotated , tenzij default_factory is opgegeven, in welk geval het moet worden gebruikt als de standaardwaarde van het veld:

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

De alias kan normaal bij de binnenhuisarchitect worden gebruikt.

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)

Gebruik met mypy

De validate_call decorateur zou "out of the box" moeten werken met mypy, omdat deze is gedefinieerd om een functie terug te geven met dezelfde handtekening als de functie die hij versiert. De enige beperking is dat, aangezien we Mypy laten denken dat de door de decorateur geretourneerde functie dezelfde is als de functie die wordt gedecoreerd; voor toegang tot de onbewerkte functie of andere attributen is type: ignore .

Ruwe functie

De onbewerkte functie die is ingericht is toegankelijk, dit is handig als u in sommige scenario's uw invoerargumenten vertrouwt en de functie op de meest performante manier wilt aanroepen (zie opmerkingen over prestaties hieronder):

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 functies

@validate_call kan ook worden gebruikt voor asynchrone functies:

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

Aangepaste configuratie

Het model achter @validate_call kan worden aangepast met behulp van een config instelling, die gelijk is aan het instellen van de subklasse ConfigDict in normale modellen.

De configuratie wordt ingesteld met behulp van het trefwoord config voor de decorateur. Het kan een configuratieklasse zijn of een dictaat van eigenschappen die later naar een klasse worden geconverteerd.

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

Extensie — argumenten valideren voordat een functie wordt aangeroepen

In sommige gevallen kan het nuttig zijn om de validatie van de argumenten van een functie te scheiden van de functieaanroep zelf. Dit kan handig zijn als een bepaalde functie kostbaar/tijdrovend is.

Hier is een voorbeeld van een oplossing die u voor dat patroon kunt gebruiken:

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

Beperkingen

Validatie-uitzondering

Momenteel wordt bij een mislukte validatie een standaard Pydantic ValidationError gegenereerd. Zie modelfoutafhandeling voor meer informatie.

Dit is handig omdat de str() methode nuttige details biedt over de fout die is opgetreden en methoden als .errors() en .json() nuttig kunnen zijn bij het blootstellen van de fouten aan eindgebruikers. ValidationError erft echter van ValueError en niet van TypeError , wat onverwacht kan zijn omdat Python een TypeError zou genereren bij ongeldige of ontbrekende argumenten. Dit kan in de toekomst worden verholpen door een aangepaste fout toe te staan of standaard een andere uitzondering te genereren, of door beide.

Dwang en strengheid

Pydantic leunt momenteel aan de kant van het proberen typen af te dwingen in plaats van een fout te genereren als een type verkeerd is, zie modelgegevensconversie en @validate_call is niet anders.

Prestatie

We hebben veel moeite gedaan om Pydantic zo performant mogelijk te maken en argumentinspectie en het maken van modellen worden slechts één keer uitgevoerd wanneer de functie is gedefinieerd, maar er zal nog steeds een prestatie-impact zijn bij het gebruik van de @validate_call decorator in vergelijking met het aanroepen van de onbewerkte functie .

In veel situaties zal dit weinig of geen merkbaar effect hebben. Houd er echter rekening mee dat @validate_call geen equivalent of alternatief is voor functiedefinities in sterk getypeerde talen; dat zal het nooit zijn.

Retourwaarde

De retourwaarde van de functie kan optioneel worden gevalideerd aan de hand van de annotatie van het retourtype.


本文总阅读量