Zum Inhalt

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 und regex vom Dekorateur in ein Path -Objekt bzw. einen Regex konvertiert.
  • max hat keine Typanmerkung und wird daher vom Dekorateur als Any 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.


本文总阅读量