sind so einfach zu bedienen, dass man sie auch leicht falsch verwenden kann, etwa wenn man einen Hammer am Kopf hält. Das Gleiche gilt für Pydantic, eine leistungsstarke Datenvalidierungsbibliothek für Python.

In Pydantic v2 ist die Kernvalidierungs-Engine implementiert RostDamit ist es eine der schnellsten Datenvalidierungslösungen im Python-Ökosystem. Dieser Leistungsvorteil wird jedoch nur dann realisiert, wenn Sie Pydantic auf eine Weise verwenden, die diesen hochoptimierten Kern tatsächlich nutzt.

Dieser Artikel konzentriert sich auf die effiziente Nutzung von Pydantic, insbesondere bei der Validierung großer Datenmengen. Wir heben vier häufige Fallstricke hervor, die zu Leistungsunterschieden in der Größenordnung führen können, wenn sie nicht aktiviert werden.


1) Bevorzugen Annotated Einschränkungen für Feldvalidatoren

Ein Kernmerkmal von Pydantic besteht darin, dass die Datenvalidierung deklarativ in einer Modellklasse definiert wird. Wenn ein Modell instanziiert wird, analysiert und validiert Pydantic die Eingabedaten entsprechend den für diese Klasse definierten Feldtypen und Validatoren.

Der naive Ansatz: Feldvalidatoren

Wir verwenden a @field_validator um Daten zu validieren, z. B. um zu prüfen, ob eine id Spalte ist tatsächlich eine Ganzzahl oder größer als Null. Dieser Stil ist lesbar und flexibel, geht jedoch mit Leistungseinbußen einher.

class UserFieldValidators(BaseModel):
    id: int
    electronic mail: EmailStr
    tags: listing(str)

    @field_validator("id")
    def _validate_id(cls, v: int) -> int:
        if not isinstance(v, int):
            increase TypeError("id have to be an integer")
        if v < 1:
            increase ValueError("id have to be >= 1")
        return v

    @field_validator("electronic mail")
    def _validate_email(cls, v: str) -> str:
        if not isinstance(v, str):
            v = str(v)
        if not _email_re.match(v):
            increase ValueError("invalid electronic mail format")
        return v

    @field_validator("tags")
    def _validate_tags(cls, v: listing(str)) -> listing(str):
        if not isinstance(v, listing):
            increase TypeError("tags have to be an inventory")
        if not (1 <= len(v) <= 10):
            increase ValueError("tags size have to be between 1 and 10")
        for i, tag in enumerate(v):
            if not isinstance(tag, str):
                increase TypeError(f"tag({i}) have to be a string")
            if tag == "":
                increase ValueError(f"tag({i}) should not be empty")

Der Grund dafür ist, dass Feldvalidatoren in ausgeführt werden Python, nach Kerntyp-Zwang und Einschränkungsvalidierung. Dies verhindert, dass sie optimiert oder in die Kernvalidierungspipeline integriert werden.

Der optimierte Ansatz: Annotated

Wir können verwenden Annotated von Python typing Bibliothek.

class UserAnnotated(BaseModel):
    id: Annotated(int, Discipline(ge=1))
    electronic mail: Annotated(str, Discipline(sample=RE_EMAIL_PATTERN))
    tags: Annotated(listing(str), Discipline(min_length=1, max_length=10))

Diese Model ist kürzer, klarer und zeigt eine schnellere Ausführung im großen Maßstab.

Warum Annotated ist schneller

Annotated (PEP 593) ist eine Normal-Python-Funktion von typing Bibliothek. Die darin platzierten Einschränkungen Annotated werden in das interne Schema von Pydantic kompiliert und im Pydantic-Core (Rust) ausgeführt.

Dies bedeutet, dass während der Validierung keine benutzerdefinierten Python-Validierungsaufrufe erforderlich sind. Außerdem werden keine Python-Zwischenobjekte oder benutzerdefinierten Kontrollflüsse eingeführt.

Im Gegensatz dazu @field_validator Funktionen stets Wenn sie in Python ausgeführt werden, führen sie zu einem Funktionsaufruf-Overhead und häufig zu doppelten Prüfungen, die bei der Kernvalidierung hätten durchgeführt werden können.

Wichtige Nuance

Eine wichtige Nuance ist das Annotated selbst ist nicht „Rust“. Die Beschleunigung entsteht durch die Verwendung von Einschränkungen, die Pydantic-Core versteht und verwenden kann, nicht durch Annotated für sich allein existierend.

Benchmark

Der Unterschied zwischen keine Validierung Und <sturdy>Annotated</sturdy> Validierung ist in diesen Benchmarks vernachlässigbar, während Python-Validatoren einen Unterschied in der Größenordnung ausmachen können.

Validierungsleistungsdiagramm (Bild vom Autor)
                    Benchmark (time in seconds)                     
┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Methodology         ┃     n=100 ┃     n=1k ┃     n=10k ┃     n=50k ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━┩
│ FieldValidators│     0.004 │    0.020 │     0.194 │     0.971 │
│ No Validation  │     0.000 │    0.001 │     0.007 │     0.032 │
│ Annotated      │     0.000 │    0.001 │     0.007 │     0.036 │
└────────────────┴───────────┴──────────┴───────────┴───────────┘

In absoluten Zahlen erreichen wir eine Validierungszeit von quick einer Sekunde auf 36 Millisekunden. Eine Leistungssteigerung um quick das 30-fache.

Urteil

Verwenden Annotated wann immer möglich. Du bekommst bessere Leistung Und klarere Modelle. Benutzerdefinierte Validatoren sind leistungsstark, aber Sie zahlen für diese Flexibilität in den Laufzeitkosten, additionally reservieren Sie @field_validator für Logik, die nicht als Einschränkungen ausgedrückt werden kann.


2). Validieren Sie JSON mit model_validate_json()

Wir haben Daten in Type eines JSON-Strings. Was ist der effizienteste Weg, diese Daten zu validieren?

Der naive Ansatz

Analysieren Sie einfach den JSON und validieren Sie das Wörterbuch:

py_dict = json.hundreds(j)
UserAnnotated.model_validate(py_dict)

Der optimierte Ansatz

Verwenden Sie eine Pydantic-Funktion:

UserAnnotated.model_validate_json(j)

Warum das schneller ist

  • model_validate_json() analysiert JSON und validiert es in einer Pipeline
  • Es verwendet Pydantic intern und einen schnelleren JSON-Parser
  • Es vermeidet den Aufbau großer Python-Zwischenwörterbücher und das zweite Durchlaufen dieser Wörterbücher während der Validierung

Mit json.hundreds() Sie zahlen doppelt: zuerst für das Parsen von JSON in Python-Objekte, dann für die Validierung und Erzwingung dieser Objekte.

model_validate_json() Reduziert Speicherzuweisungen und redundantes Durchlaufen.

Benchmarked

Die Pydantic-Model ist quick doppelt so schnell.

Leistungsdiagramm (Bild vom Autor)
                  Benchmark (time in seconds)                   
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Methodology              ┃ n=100 ┃  n=1K ┃ n=10K ┃ n=50K ┃ n=250K ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ Load json           │ 0.000 │ 0.002 │ 0.016 │ 0.074 │  0.368 │
│ mannequin validate json │ 0.001 │ 0.001 │ 0.009 │ 0.042 │  0.209 │
└─────────────────────┴───────┴───────┴───────┴───────┴────────┘

In absoluten Zahlen sparen wir durch die Änderung 0,1 Sekunden bei der Validierung einer Viertelmillion Objekte.

Urteil

Wenn Ihre Eingabe JSON ist, überlassen Sie Pydantic die Analyse und Validierung in einem Schritt. Aus Leistungsgründen ist die Verwendung nicht unbedingt erforderlich model_validate_json() aber tun Sie dies trotzdem, um die Erstellung von Python-Zwischenobjekten zu vermeiden und Ihren Code zu komprimieren.


3) Verwendung TypeAdapter zur Massenvalidierung

Wir haben ein Consumer Modell und jetzt wollen wir a validieren listing von ConsumerS.

Der naive Ansatz

Wir können die Liste durchlaufen und jeden Eintrag validieren oder ein Wrapper-Modell erstellen. Annehmen batch ist ein listing(dict):

# 1. Per-item validation
fashions = (Consumer.model_validate(merchandise) for merchandise in batch)

# 2. Wrapper mannequin


# 2.1 Outline a wrapper mannequin:
class UserList(BaseModel):
  customers: listing(Consumer)


# 2.2 Validate with the wrapper mannequin
fashions = UserList.model_validate({"customers": batch}).customers

Optimierter Ansatz

Typadapter sind für die Validierung von Objektlisten schneller.

ta_annotated = TypeAdapter(listing(UserAnnotated))
fashions = ta_annotated.validate_python(batch)

Warum das schneller ist

Überlassen Sie die schwere Arbeit Rust. Für die Verwendung eines TypeAdapter ist die Erstellung eines zusätzlichen Wrappers nicht erforderlich, und die Validierung wird mit einem ausgeführt einzelnes kompiliertes Schema. Es gibt weniger Grenzübergänge von Python nach Rust und zurück und es entsteht ein geringerer Aufwand für die Objektzuweisung.

Wrapper-Modelle sind langsamer, weil sie mehr als nur die Liste validieren:

  • Konstruiert eine zusätzliche Modellinstanz
  • Verfolgt Feldsätze und internen Standing
  • Behandelt Konfiguration, Standardeinstellungen und Extras

Diese zusätzliche Ebene ist professional Aufruf klein, wird aber im Maßstab messbar.

Benchmarked

Bei der Verwendung großer Units stellen wir fest, dass der Typ-Adapter deutlich schneller ist, insbesondere im Vergleich zum Wrapper-Modell.

Leistungsdiagramm (Bild vom Autor)
                   Benchmark (time in seconds)                    
┏━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Methodology       ┃ n=100 ┃  n=1K ┃ n=10K ┃ n=50K ┃ n=100K ┃ n=250K ┃
┡━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ Per-item     │ 0.000 │ 0.001 │ 0.021 │ 0.091 │  0.236 │  0.502 │
│ Wrapper mannequin│ 0.000 │ 0.001 │ 0.008 │ 0.108 │  0.208 │  0.602 │
│ TypeAdapter  │ 0.000 │ 0.001 │ 0.021 │ 0.083 │  0.152 │  0.381 │
└──────────────┴───────┴───────┴───────┴───────┴────────┴────────┘

In absoluten Zahlen sparen wir durch die Beschleunigung jedoch etwa 120 bis 220 Millisekunden bei 250.000 Objekten.

Urteil

Wenn Sie nur einen Typ validieren und kein Domänenobjekt definieren möchten, TypeAdapter ist die schnellste und sauberste Choice. Obwohl dies aus Gründen der Zeitersparnis nicht unbedingt erforderlich ist, überspringt es unnötige Modellinstanziierungen und vermeidet Python-seitige Validierungsschleifen, wodurch Ihr Code sauberer und besser lesbar wird.


4) Vermeiden from_attributes es sei denn, Sie brauchen es

Mit from_attributes Sie konfigurieren Ihre Modellklasse. Wenn Sie es einstellen True Sie weisen Pydantic an, Werte aus Objektattributen statt aus Wörterbuchschlüsseln zu lesen. Dies ist wichtig, wenn Ihre Eingabe etwas anderes als ein Wörterbuch ist, wie etwa eine SQLAlchemy ORM-Instanz, eine Datenklasse oder ein beliebiges einfaches Python-Objekt mit Attributen.

Standardmäßig from_attributes Ist False. Manchmal setzen Entwickler dieses Attribut auf True Um das Modell flexibel zu halten:

class Product(BaseModel):
    id: int
    identify: str

    model_config = ConfigDict(from_attributes=True)

Wenn Sie jedoch nur Wörterbücher an Ihr Modell übergeben, sollten Sie dies am besten vermeiden from_attributes weil es erfordert, dass Python viel mehr Arbeit leistet. Der resultierende Overhead bietet keinen Vorteil, wenn sich die Eingabe bereits in der einfachen Zuordnung befindet.

Warum from_attributes=True ist langsamer

Diese Methode verwendet getattr() anstelle der Wörterbuchsuche, die langsamer ist. Außerdem kann es Funktionen für das Objekt auslösen, von dem wir lesen, wie Deskriptoren, Eigenschaften oder ORM-Lazy-Loading.

Benchmark

Mit zunehmender Batch-Größe wird die Verwendung von Attributen immer teurer.

Leistungsdiagramm (Bild vom Autor)
                       Benchmark (time in seconds)                        
┏━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Methodology       ┃ n=100 ┃  n=1K ┃ n=10K ┃ n=50K ┃ n=100K ┃ n=250K ┃
┡━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ with attribs │ 0.000 │ 0.001 │ 0.011 │ 0.110 │  0.243 │  0.593 │
│ no attribs   │ 0.000 │ 0.001 │ 0.012 │ 0.103 │  0.196 │  0.459 │
└──────────────┴───────┴───────┴───────┴───────┴────────┴────────┘

In absoluten Zahlen werden bei der Validierung von 250.000 Objekten etwas weniger als 0,1 Sekunden eingespart.

Urteil

Nur verwenden from_attributes wenn Ihre Eingabe ist kein Diktat. Es dient zur Unterstützung attributbasierter Objekte (ORMs, Datenklassen, Domänenobjekte). In diesen Fällen kann es schneller sein, als das Objekt zunächst in ein Diktat zu übertragen und es dann zu validieren. Bei einfachen Zuordnungen erhöht sich der Overhead ohne Nutzen.


Abschluss

Der Sinn dieser Optimierungen besteht nicht darin, um ihrer selbst willen ein paar Millisekunden einzusparen. In absoluten Zahlen ist selbst ein Unterschied von 100 ms selten der Flaschenhals in einem realen System.

Der wahre Wert liegt darin, klareren Code zu schreiben und Ihre Instruments richtig zu verwenden.

Die Verwendung der in diesem Artikel angegebenen Tipps führt zu klarere Modellemehr explizite Absichtund a bessere Ausrichtung mit der Funktionsweise von Pydantic. Diese Muster verschieben die Validierungslogik aus Advert-hoc-Python-Code in deklarative Schemata einfacher zu lesen, zu begründen und zu warten.

Die Leistungsverbesserungen sind ein Nebeneffekt des Handelns der richtige Weg. Wenn Validierungsregeln deklarativ ausgedrückt werden, kann Pydantic sie konsistent anwenden, intern optimieren und auf natürliche Weise skalieren, wenn Ihre Daten wachsen.

Zusamenfassend:

Übernehmen Sie diese Muster nicht, nur weil sie schneller sind. Übernehmen Sie sie, weil sie Ihren Code einfacher, expliziter und besser für die von Ihnen verwendeten Instruments geeignet machen.

Die Beschleunigung ist einfach ein netter Bonus.


Ich hoffe, dieser Artikel conflict so klar, wie ich es beabsichtigt hatte. Sollte dies jedoch nicht der Fall sein, teilen Sie mir bitte mit, was ich zur weiteren Klärung tun kann. Schauen Sie sich in der Zwischenzeit meine an andere Artikel zu allen möglichen Programmierthemen.

Viel Spaß beim Codieren!

– Mike

Ps: Gefällt mir, was ich mache? Folgen Sie mir!

Von admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert