Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 649 – Deferred Evaluation Of Annotations Using Descriptors

Autor:
Larry Hastings <larry at hastings.org>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
11. Jan 2021
Python-Version:
3.14
Post-History:
11. Jan 2021, 12. Apr 2021, 18. Apr 2021, 09. Aug 2021, 20. Okt 2021, 20. Okt 2021, 17. Nov 2021, 15. Mär 2022, 23. Nov 2022, 07. Feb 2023, 11. Apr 2023
Ersetzt:
563
Resolution:
08. Mai 2023

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie nun unter Annotationen.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Annotationen sind eine Python-Technologie, die es ermöglicht, Typinformationen und andere Metadaten über Python-Funktionen, Klassen und Module auszudrücken. Die ursprüngliche Semantik von Python für Annotationen erforderte jedoch, dass diese sofort ausgewertet wurden, zum Zeitpunkt, an dem das annotierte Objekt gebunden wurde. Dies führte zu chronischen Problemen für Benutzer der statischen Typanalyse, die „Type Hints“ verwendeten, aufgrund von Problemen mit Vorwärts- und Zirkelreferenzen.

Python löste dieses Problem durch die Annahme von PEP 563, das einen neuen Ansatz namens „Stringized Annotations“ (String-Annotationen) einführte, bei dem Annotationen automatisch von Python in Strings umgewandelt wurden. Dies löste die Probleme mit Vorwärts- und Zirkelreferenzen und ermöglichte auch faszinierende neue Anwendungen für Annotationsmetadaten. String-Annotationen verursachten jedoch ihrerseits chronische Probleme für Laufzeitbenutzer von Annotationen.

Dieses PEP schlägt einen neuen und umfassenden dritten Ansatz für die Darstellung und Berechnung von Annotationen vor. Es fügt einen neuen internen Mechanismus hinzu, um Annotationen bei Bedarf verzögert zu berechnen, über eine neue Objektmethode namens __annotate__. Dieser Ansatz löst in Kombination mit einer neuartigen Technik zur Umwandlung von Annotationswerten in alternative Formate alle oben genannten Probleme, unterstützt alle bestehenden Anwendungsfälle und soll zukünftige Innovationen bei Annotationen fördern.

Übersicht

Dieses PEP fügt den Objekten, die Annotationen unterstützen – Funktionen, Klassen und Module – ein neues Dunder-Attribut hinzu. Das neue Attribut heißt __annotate__ und ist eine Referenz auf eine Funktion, die das Annotations-Dictionary des Objekts berechnet und zurückgibt.

Zur Kompilierzeit schreibt der Python-Compiler, wenn die Definition eines Objekts Annotationen enthält, die Ausdrücke, die die Annotationen berechnen, in eine eigene Funktion. Wenn diese Funktion ausgeführt wird, gibt sie das Annotations-Dictionary zurück. Der Python-Compiler speichert dann eine Referenz auf diese Funktion unter __annotate__ auf dem Objekt.

Darüber hinaus wird __annotations__ als „Data Descriptor“ neu definiert, der diese Annotationsfunktion einmal aufruft und das Ergebnis zwischenspeichert.

Dieser Mechanismus verzögert die Auswertung von Annotationsausdrücken, bis die Annotationen abgefragt werden, was viele Probleme mit Zirkelreferenzen löst.

Dieses PEP definiert auch neue Funktionalitäten für zwei Funktionen in der Python-Standardbibliothek: inspect.get_annotations und typing.get_type_hints. Die Funktionalität wird über einen neuen, nur-keyword-Parameter namens format aufgerufen. format ermöglicht es dem Benutzer, die Annotationen von diesen Funktionen in einem bestimmten Format anzufordern. Formatbezeichner sind immer vordefinierte Ganzzahlwerte. Die von diesem PEP definierten Formate sind:

  • inspect.VALUE = 1

    Der Standardwert. Die Funktion gibt die konventionellen Python-Werte für die Annotationen zurück. Dieses Format ist identisch mit dem Rückgabewert dieser Funktionen unter Python 3.11.

  • inspect.FORWARDREF = 2

    Die Funktion versucht, die konventionellen Python-Werte für die Annotationen zurückzugeben. Wenn sie jedoch auf einen undefinierten Namen oder eine freie Variable stößt, die noch keinem Wert zugeordnet wurde, erstellt sie dynamisch ein Proxy-Objekt (ein ForwardRef), das diesen Wert im Ausdruck ersetzt, und fährt dann mit der Auswertung fort. Das resultierende Dictionary kann eine Mischung aus Proxys und echten Werten enthalten. Wenn alle echten Werte zum Zeitpunkt des Aufrufs der Funktion definiert sind, liefern inspect.FORWARDREF und inspect.VALUE identische Ergebnisse.

  • inspect.SOURCE = 3

    Die Funktion erzeugt ein Annotations-Dictionary, bei dem die Werte durch Strings ersetzt wurden, die den ursprünglichen Quellcode für die Annotationsausdrücke enthalten. Diese Strings können nur annähernd sein, da sie aus einem anderen Format zurückentwickelt sein können, anstatt den ursprünglichen Quellcode zu bewahren, aber die Unterschiede werden gering sein.

Wenn dieses PEP angenommen wird, würde es PEP 563 ersetzen und das Verhalten von PEP 563 würde als veraltet markiert und schließlich entfernt werden.

Vergleich der Semantik von Annotationen

Hinweis

Der in diesem Abschnitt dargestellte Code ist zur besseren Übersichtlichkeit vereinfacht und in einigen kritischen Aspekten absichtlich ungenau. Dieses Beispiel dient lediglich dazu, die übergeordneten Konzepte zu vermitteln, ohne sich in den Details zu verlieren. Die Leser sollten jedoch beachten, dass die tatsächliche Implementierung in mehrfacher Hinsicht erheblich abweicht. Abschnitt Implementierung weiter unten in diesem PEP bietet eine weitaus genauere Beschreibung dessen, was dieses PEP auf technischer Ebene vorschlägt.

Betrachten Sie dieses Beispiel-Code

def foo(x: int = 3, y: MyType = None) -> float:
    ...
class MyType:
    ...
foo_y_annotation = foo.__annotations__['y']

Wie wir hier sehen, sind Annotationen zur Laufzeit über ein Attribut __annotations__ auf Funktionen, Klassen und Modulen verfügbar. Wenn Annotationen für eines dieser Objekte angegeben werden, ist __annotations__ ein Dictionary, das die Namen der Felder den als Feldannotationen angegebenen Werten zuordnet.

Das Standardverhalten in Python besteht darin, die Ausdrücke für die Annotationen auszuwerten und das Annotations-Dictionary zu erstellen, wenn die Funktion, Klasse oder das Modul gebunden wird. Zur Laufzeit funktioniert der obige Code tatsächlich ungefähr so:

annotations = {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
    ...
foo.__annotations__ = annotations
class MyType:
    ...
foo_y_annotation = foo.__annotations__['y']

Das entscheidende Detail hier ist, dass die Werte int, MyType und float zum Zeitpunkt der Bindung des Funktions-Objekts nachgeschlagen werden und diese Werte im Annotations-Dictionary gespeichert werden. Dieser Code läuft jedoch nicht – er wirft einen NameError in der ersten Zeile, weil MyType noch nicht definiert ist.

Die Lösung von PEP 563 besteht darin, die Ausdrücke während der Kompilierung zurück in Strings zu dekompilieren und diese Strings als Werte im Annotations-Dictionary zu speichern. Der entsprechende Laufzeit-Code würde ungefähr so aussehen:

annotations = {'x': 'int', 'y': 'MyType', 'return': 'float'}
def foo(x = 3, y = "abc"):
    ...
foo.__annotations__ = annotations
class MyType:
    ...
foo_y_annotation = foo.__annotations__['y']

Dieser Code läuft nun erfolgreich. Allerdings ist foo_y_annotation nicht mehr eine Referenz auf MyType, sondern der *String* 'MyType'. Um den String in den tatsächlichen Wert MyType umzuwandeln, müsste der Benutzer den String mit eval, inspect.get_annotations oder typing.get_type_hints auswerten.

Dieses PEP schlägt einen dritten Ansatz vor: die verzögerte Auswertung der Annotationen durch deren Ausführung in einer eigenen Funktion. Wenn dieses PEP aktiv wäre, würde der generierte Code ungefähr so funktionieren:

class function:
    # __annotations__ on a function object is already a
    # "data descriptor" in Python, we're just changing
    # what it does
    @property
    def __annotations__(self):
        return self.__annotate__()

# ...

def annotate_foo():
    return {'x': int, 'y': MyType, 'return': float}
def foo(x = 3, y = "abc"):
    ...
foo.__annotate__ = annotate_foo
class MyType:
   ...
foo_y_annotation = foo.__annotations__['y']

Die wesentliche Änderung besteht darin, dass der Code zur Erstellung des Annotations-Dictionaries nun in einer Funktion lebt – hier annotate_foo() genannt. Aber diese Funktion wird erst aufgerufen, wenn wir nach dem Wert von foo.__annotations__ fragen, und das tun wir erst, *nachdem* MyType definiert wurde. Dieser Code läuft also auch erfolgreich, und foo_y_annotation hat nun den korrekten Wert – die Klasse MyType –, obwohl MyType erst *nach* der Definition der Annotation definiert wurde.

Fehlerhafte Zurückweisung dieses Ansatzes im November 2017

Während der frühen Diskussionen über PEP 563, in einem Thread im November 2017 in comp.lang.python-dev, wurde die Idee, Code zur verzögerten Auswertung von Annotationen zu verwenden, kurz diskutiert. Damals wurde die Technik als „impliziter Lambda-Ausdruck“ bezeichnet.

Guido van Rossum – damals Python's BDFL – antwortete und behauptete, dass diese „impliziten Lambda-Ausdrücke“ nicht funktionieren würden, da sie Symbole nur im Modul-Scope auflösen könnten.

Meiner Meinung nach tötet die Unfähigkeit, klasseninterne Definitionen aus Annotationen von Methoden zu referenzieren, diese Idee praktisch.

https://mail.python.org/pipermail/python-dev/2017-November/150109.html

Dies führte zu einer kurzen Diskussion darüber, Lambda-basierte Annotationen für Methoden zu erweitern, um auf klasseninterne Definitionen verweisen zu können, indem eine Referenz auf den klasseninternen Scope beibehalten wird. Auch diese Idee wurde schnell verworfen.

PEP 563 fasst die obige Diskussion zusammen

Der Ansatz dieses PEP leidet nicht unter diesen Einschränkungen. Annotationen können auf Modul-Level-Definitionen, Klassen-Level-Definitionen und sogar lokale und freie Variablen zugreifen.

Motivation

Eine Geschichte der Annotationen

Python 3.0 wurde mit einer neuen Syntaxfunktion ausgeliefert, „Annotationen“, die in PEP 3107 definiert ist. Diese ermöglichte die Angabe eines Python-Wertes, der einem Parameter einer Python-Funktion oder dem Rückgabewert dieser Funktion zugeordnet wurde. Anders ausgedrückt: Annotationen gaben Python-Benutzern eine Schnittstelle, um reichhaltige Metadaten über einen Funktionsparameter oder Rückgabewert bereitzustellen, z. B. Typinformationen. Alle Annotationen für eine Funktion wurden zusammen in einem neuen Attribut __annotations__ gespeichert, in einem „Annotations-Dictionary“, das Parameternamen (oder im Fall der Rückgabenotationen unter dem Namen 'return') ihren Python-Werten zuordnete.

Um Experimente zu fördern, legte Python bewusst nicht fest, welche Form diese Metadaten haben sollten oder welche Werte verwendet werden sollten. Benutzer-Code begann fast sofort mit dieser neuen Einrichtung zu experimentieren. Beliebte Bibliotheken, die diese Funktionalität nutzten, entstanden jedoch nur langsam.

Nach Jahren geringen Fortschritts wählte der BDFL einen bestimmten Ansatz zur Darstellung statischer Typinformationen, die sogenannten *Type Hints*, wie in PEP 484 definiert. Python 3.5 wurde mit einem neuen typing-Modul ausgeliefert, das schnell sehr populär wurde.

Python 3.6 fügte Syntax hinzu, um lokale Variablen, Klassenattribute und Modulattribute zu annotieren, unter Verwendung des in PEP 526 vorgeschlagenen Ansatzes. Die statische Typanalyse erfreute sich weiterhin wachsender Beliebtheit.

Benutzer der statischen Typanalyse waren jedoch zunehmend frustriert von einem unbequemen Problem: Vorwärtsreferenzen. Im klassischen Python ist es normalerweise kein Problem, wenn eine Klasse C von einer später definierten Klasse D abhängt, da der Benutzer-Code normalerweise wartet, bis beide definiert sind, bevor er versucht, eine von ihnen zu verwenden. Annotationen fügten jedoch eine neue Komplikation hinzu, da sie zum Zeitpunkt der Bindung des annotierten Objekts (Funktion, Klasse oder Modul) berechnet wurden. Wenn Methoden in Klasse C mit Typ D annotiert sind und diese Annotationsausdrücke zum Zeitpunkt der Bindung der Methode berechnet werden, ist D möglicherweise noch nicht definiert. Und wenn Methoden in D ebenfalls mit Typ C annotiert sind, haben Sie nun ein unlösbares Problem mit Zirkelreferenzen.

Anfänglich umgingen Benutzer der statischen Typisierung dieses Problem, indem sie ihre problematischen Annotationen als Strings definierten. Dies funktionierte, da ein String, der den Type Hint enthielt, für das statische Typanalysetool genauso nützlich war. Und Benutzer von statischen Typanalysetools untersuchen die Annotationen zur Laufzeit selten, sodass diese Darstellung selbst keine Unannehmlichkeit darstellte. Das manuelle Umwandeln von Type Hints in Strings war jedoch umständlich und fehleranfällig. Außerdem wurden in Codebasen immer mehr Annotationen hinzugefügt, was mehr und mehr CPU-Zeit für deren Erstellung und Bindung benötigte.

Um diese Probleme zu lösen, akzeptierte der BDFL PEP 563, das eine neue Funktion in Python 3.7 einführte: „Stringized Annotations“. Sie wurde mit einem Zukunfts-Import aktiviert:

from __future__ import annotations

Normalerweise wurden Annotationsausdrücke zum Zeitpunkt der Bindung des Objekts ausgewertet, wobei ihre Werte im Annotations-Dictionary gespeichert wurden. Wenn String-Annotationen aktiv waren, änderten sich diese Semantiken: Stattdessen wandelte der Compiler zur Kompilierzeit alle Annotationen in diesem Modul in String-Darstellungen ihres Quellcodes um – und verwandelte damit *automatisch* die Annotationen der Benutzer in Strings, wodurch die Notwendigkeit, sie *manuell* in Strings umzuwandeln, entfiel. PEP 563 schlug vor, dass Benutzer diesen String mit eval auswerten könnten, wenn der tatsächliche Wert zur Laufzeit benötigt wurde.

(Von nun an wird dieses PEP die klassischen Semantiken von PEP 3107 und PEP 526, bei denen die Werte von Annotationsausdrücken zum Zeitpunkt der Bindung des Objekts berechnet werden, als *„Stock“-Semantik* bezeichnen, um sie von der neuen PEP 563 „Stringized“ Annotationssemantik zu unterscheiden.)

Der aktuelle Stand der Anwendungsfälle für Annotationen

Obwohl es viele spezifische Anwendungsfälle für Annotationen gibt, tendierten Annotationsbenutzer in der Diskussion um dieses PEP dazu, in eine der folgenden vier Kategorien zu fallen.

Benutzer statischer Typisierung

Benutzer von statischer Typisierung verwenden Annotationen, um ihren Code mit Typinformationen zu versehen. Sie untersuchen die Annotationen jedoch weitgehend nicht zur Laufzeit. Stattdessen verwenden sie statische Typanalysetools (mypy, pytype), um ihren Quellcodebaum zu untersuchen und festzustellen, ob ihr Code konsistent mit Typen umgeht. Dies ist heute mit ziemlicher Sicherheit der beliebteste Anwendungsfall für Annotationen.

Viele der Annotationen verwenden *Type Hints*, ähnlich wie in PEP 484 (und vielen nachfolgenden PEPs). Type Hints sind passive Objekte, bloße Repräsentationen von Typinformationen; sie führen keine eigentliche Arbeit aus. Type Hints werden oft mit anderen Typen oder anderen Type Hints parametrisiert. Da sie keine Angaben über die tatsächlichen Werte machen, funktionieren Type Hints gut mit ForwardRef-Proxy-Objekten. Benutzer von statischen Type Hints stellten fest, dass extensive Type Hinting unter Stock-Semantik häufig zu zirkulären Referenz- und zirkulären Importproblemen im großen Stil führte, die schwer zu lösen waren. PEP 563 wurde speziell entwickelt, um dieses Problem zu lösen, und die Lösung funktionierte für diese Benutzer hervorragend. Die Schwierigkeit, String-Annotationen in reale Werte umzuwandeln, beeinträchtigte diese Benutzer weitgehend nicht, da sie Annotationen nur selten zur Laufzeit untersuchten.

Benutzer statischer Typisierung kombinieren häufig PEP 563 mit dem Idiom if typing.TYPE_CHECKING, um zu verhindern, dass ihre Type Hints zur Laufzeit geladen werden. Das bedeutet, dass sie oft ihre String-Annotationen nicht auswerten und zur Laufzeit keine realen Werte erzeugen können. In den seltenen Fällen, in denen sie Annotationen zur Laufzeit untersuchen, verzichten sie oft auf eval und verwenden stattdessen lexikalische Analyse direkt auf den String-Annotationen.

Unter diesem PEP werden Benutzer statischer Typisierung wahrscheinlich das Format FORWARDREF oder SOURCE bevorzugen.

Benutzer von Laufzeit-Annotationen

Benutzer von Laufzeit-Annotationen verwenden Annotationen als Mittel zur Darstellung reicher Metadaten über ihre Funktionen und Klassen, die sie als Eingabe für Laufzeitverhalten verwenden. Spezifische Anwendungsfälle sind Laufzeit-Typprüfung (Pydantic) und Glue-Logik zur Exposition von Python-APIs in einer anderen Domäne (FastAPI, Typer). Die Annotationen können Type Hints sein oder auch nicht.

Da Benutzer von Laufzeit-Annotationen Annotationen zur Laufzeit untersuchen, wurden sie traditionell besser mit Stock-Semantik bedient. Dieser Anwendungsfall ist weitgehend inkompatibel mit PEP 563, insbesondere mit dem Idiom if typing.TYPE_CHECKING.

Unter diesem PEP werden Benutzer von Laufzeit-Annotationen höchstwahrscheinlich das Format VALUE bevorzugen, obwohl einige (z. B. wenn sie Annotationen in einem Dekorator eilig auswerten und Vorwärtsreferenzen unterstützen möchten) auch das Format FORWARDREF verwenden könnten.

Wrapper

Wrapper sind Funktionen oder Klassen, die Benutzerfunktionen oder -klassen umschließen und Funktionalität hinzufügen. Beispiele hierfür wären dataclass(), functools.partial(), attrs und wrapt.

Wrapper sind eine eigenständige Unterkategorie von Benutzern von Laufzeit-Annotationen. Obwohl sie Annotationen zur Laufzeit verwenden, untersuchen sie möglicherweise die Annotationen der von ihnen umschlossenen Objekte nicht tatsächlich – es hängt von der Funktionalität ab, die der Wrapper bietet. Grundsätzlich sollten sie die Annotationen des umschlossenen Objekts an den von ihnen erstellten Wrapper weitergeben, obwohl sie diese Annotationen möglicherweise ändern können.

Wrapper wurden im Allgemeinen so konzipiert, dass sie unter Stock-Semantik gut funktionieren. Ob sie unter PEP 563-Semantik gut funktionieren, hängt davon ab, inwieweit sie die Annotationen des umschlossenen Objekts untersuchen. Oft kümmert sich der Wrapper nicht um den Wert an sich, sondern benötigt nur bestimmte Informationen über die Annotationen. Dennoch können PEP 563 und das Idiom if typing.TYPE_CHECKING es Wrappern erschweren, die benötigten Informationen zur Laufzeit zuverlässig zu ermitteln. Dies ist ein fortlaufendes, chronisches Problem. Unter diesem PEP werden Wrapper wahrscheinlich das Format FORWARDREF für ihre interne Logik bevorzugen. Die umschlossenen Objekte müssen jedoch alle Formate für ihre Benutzer unterstützen.

Dokumentation

PEP 563 String-Annotationen waren ein Segen für Werkzeuge, die Dokumentation mechanisch erstellen.

String-basierte Type Hints eignen sich hervorragend für die Dokumentation; Type Hints, wie sie im Quellcode ausgedrückt werden, sind oft prägnant und lesbar. Zur Laufzeit können dieselben Type Hints jedoch Werte erzeugen, deren `repr` eine weitläufige, verschachtelte, unleserliche Unordnung ist. Daher waren Dokumentationsbenutzer gut mit PEP 563 bedient, aber schlecht mit Stock-Semantik.

Unter diesem PEP wird erwartet, dass Dokumentationsbenutzer das Format SOURCE verwenden.

Motivation für dieses PEP

Die ursprüngliche Semantik von Python für Annotationen machte deren Verwendung für die statische Typanalyse aufgrund von Vorwärtsreferenzproblemen schmerzhaft. PEP 563 löste das Problem der Vorwärtsreferenz, und viele Benutzer statischer Typanalyse wurden zu frühen Anwendern. Aber seine unkonventionelle Lösung schuf neue Probleme für zwei der oben genannten Anwendungsfälle: Benutzer von Laufzeit-Annotationen und Wrapper.

Erstens erlaubten String-Annotationen keine Referenzierung von lokalen oder freien Variablen, was bedeutete, dass viele nützliche, sinnvolle Ansätze zur Erstellung von Annotationen nicht mehr gangbar waren. Dies war besonders für Dekoratoren, die bestehende Funktionen und Klassen umschließen, umständlich, da diese Dekoratoren oft Closures verwenden.

Zweitens muss man, damit eval globale Variablen in einer String-Annotation korrekt nachschlagen kann, zuerst eine Referenz auf das richtige Modul erhalten. Klassenobjekte behalten jedoch keine Referenz auf ihre globalen Variablen. PEP 563 schlägt vor, das Modul einer Klasse im Namen in sys.modules nachzuschlagen – eine überraschende Anforderung für ein sprachliches Feature.

Zusätzlich können komplexe, aber legitime Konstrukte es schwierig machen, die korrekten globalen und lokalen Dictionaries zu bestimmen, die an eval übergeben werden müssen, um eine String-Annotation korrekt auszuwerten. Noch schlimmer, in einigen Situationen kann es schlichtweg unmöglich sein.

Einige Bibliotheken (z. B. typing.TypedDict, dataclasses) umschließen eine Benutzerklasse und verschmelzen dann alle Annotationen aus allen Basisklassen dieser Klasse zu einem kumulativen Annotations-Dictionary. Wenn diese Annotationen in Strings umgewandelt wurden, funktioniert ein späterer Aufruf von eval möglicherweise nicht korrekt, da das globale Dictionary, das für das eval verwendet wird, das Modul ist, in dem die *Benutzerklasse* definiert wurde, was möglicherweise nicht dasselbe Modul ist, in dem die *Annotation* definiert wurde. Wenn die Annotationen jedoch aufgrund von Vorwärtsreferenzproblemen in Strings umgewandelt wurden, funktioniert ein früher Aufruf von eval möglicherweise auch nicht, da die Vorwärtsreferenz noch nicht auflösbar ist. Dies hat sich als schwer zu vereinbaren erwiesen; von den drei unten verlinkten Fehlerberichten wurde nur einer als behoben markiert.

Selbst mit korrekten globalen *und* lokalen Variablen kann eval bei String-Annotationen unzuverlässig sein. eval kann nur erfolgreich sein, wenn alle in einer Annotation referenzierten Symbole definiert sind. Wenn eine String-Annotation auf eine Mischung aus definierten und undefinierten Symbolen verweist, schlägt ein einfaches eval dieses Strings fehl. Dies ist ein Problem für Bibliotheken, die die Annotation untersuchen müssen, da sie diese String-Annotationen nicht zuverlässig in reale Werte umwandeln können.

  • Einige Bibliotheken (z. B. dataclasses) lösten dies, indem sie auf reale Werte verzichteten und lexikalische Analyse der String-Annotation durchführten, was viel Arbeit erfordert, um es richtig zu machen.
  • Andere Bibliotheken leiden immer noch unter diesem Problem, was zu überraschendem Laufzeitverhalten führen kann. https://github.com/python/cpython/issues/97727

Außerdem ist eval() langsam und nicht immer verfügbar; es wird aus Platzgründen auf bestimmten Plattformen manchmal entfernt. eval() auf MicroPython unterstützt das Argument locals nicht, was die Umwandlung von String-Annotationen in reale Werte zur Laufzeit noch schwieriger macht.

Schließlich erfordert PEP 563, dass Python-Implementierungen ihre Annotationen in Strings umwandeln. Dies ist ein überraschendes Verhalten – beispiellos für ein sprachliches Feature, mit einer komplizierten Implementierung, die aktualisiert werden muss, wann immer ein neuer Operator zur Sprache hinzugefügt wird.

Diese Probleme motivierten die Forschung zur Suche nach einem neuen Ansatz zur Lösung der Probleme von Annotationsbenutzern, was zu diesem PEP führte.

Implementierung

Beobachtete Semantik für Annotationsausdrücke

Für jedes Objekt o, das Annotationen unterstützt, und vorausgesetzt, dass alle in den Annotationsausdrücken ausgewerteten Namen vor der Definition von o gebunden sind und nie wieder neu gebunden werden, liefert o.__annotations__ ein identisches Annotations-Dictionary, sowohl wenn „Stock“-Semantik aktiv ist als auch wenn dieses PEP aktiv ist. Insbesondere wird die Namensauflösung in beiden Szenarien identisch durchgeführt.

Wenn dieses PEP aktiv ist, wird der Wert von o.__annotations__ erst dann berechnet, wenn o.__annotations__ selbst zum ersten Mal ausgewertet wird. Die gesamte Auswertung der Annotationsausdrücke wird bis zu diesem Zeitpunkt verzögert, was auch bedeutet, dass

  • Namen, die in den Annotationsausdrücken referenziert werden, ihren *aktuellen* Wert zu diesem Zeitpunkt verwenden, und
  • wenn die Auswertung der Annotationsausdrücke eine Ausnahme auslöst, wird diese Ausnahme zu diesem Zeitpunkt ausgelöst.

Sobald o.__annotations__ zum ersten Mal erfolgreich berechnet wurde, wird dieser Wert zwischengespeichert und bei zukünftigen Anfragen nach o.__annotations__ zurückgegeben.

__annotate__ und __annotations__

Python unterstützt Annotationen für drei verschiedene Typen: Funktionen, Klassen und Module. Dieses PEP modifiziert die Semantiken für alle drei dieser Typen auf ähnliche Weise.

Erstens fügt dieses PEP ein neues „Dunder“-Attribut hinzu: __annotate__. __annotate__ muss ein „Data Descriptor“ sein, der alle drei Aktionen implementiert: get, set und delete. Das Attribut __annotate__ ist immer definiert und darf nur entweder auf None oder auf ein aufrufbares Objekt gesetzt werden. (__annotate__ kann nicht gelöscht werden.) Wenn ein Objekt keine Annotationen hat, sollte __annotate__ mit None initialisiert werden, anstatt mit einer Funktion, die ein leeres Dictionary zurückgibt.

Der Data Descriptor __annotate__ muss einen dedizierten Speicher im Objekt haben, um die Referenz auf seinen Wert zu speichern. Der Speicherort dieses Speichers zur Laufzeit ist ein Implementierungsdetail. Selbst wenn er für Python-Code sichtbar ist, sollte er dennoch als internes Implementierungsdetail betrachtet werden, und Python-Code sollte bevorzugen, nur über das Attribut __annotate__ damit zu interagieren.

Der in __annotate__ gespeicherte aufrufbare Wert muss ein einzelnes erforderliches positionsgebundenes Argument namens format akzeptieren, das immer eine int (oder eine Unterklasse von int) ist. Er muss entweder ein Dictionary (oder eine Unterklasse von Dictionary) zurückgeben oder NotImplementedError() auslösen.

Hier ist eine formale Definition von __annotate__, wie sie im Abschnitt „Magic Methods“ der Python Language Reference erscheinen wird:

__annotate__(format: int) -> dict

Gibt ein neues Dictionary-Objekt zurück, das Attribut-/Parameternamen ihren Annotationswerten zuordnet.

Akzeptiert einen Parameter format, der das Format angibt, in dem Annotationswerte bereitgestellt werden sollen. Muss einer der folgenden sein:

inspect.VALUE (entspricht der Ganzzahlkonstante 1)

Werte sind das Ergebnis der Auswertung der Annotationsausdrücke.

inspect.FORWARDREF (entspricht der Ganzzahlkonstante 2)

Werte sind echte Annotationswerte (gemäß inspect.VALUE Format) für definierte Werte und ForwardRef-Proxys für undefinierte Werte. Echte Objekte können ForwardRef-Proxy-Objekten ausgesetzt sein oder Referenzen darauf enthalten.

inspect.SOURCE (entspricht der Ganzzahlkonstante 3)

Werte sind der Text-String der Annotation, wie sie im Quellcode erscheint. Kann nur annähernd sein; Leerzeichen können normalisiert werden und konstante Werte können optimiert werden. Es ist möglich, dass die exakten Werte dieser Strings in zukünftigen Python-Versionen geändert werden.

Wenn eine __annotate__-Funktion das angeforderte Format nicht unterstützt, muss sie NotImplementedError() auslösen. __annotate__-Funktionen müssen immer das Format 1 (inspect.VALUE) unterstützen; sie dürfen NotImplementedError() nicht auslösen, wenn sie mit format=1 aufgerufen werden.

Wenn format=1 übergeben wird, kann eine __annotate__-Funktion NameError auslösen; sie darf jedoch keine NameError auslösen, wenn sie zur Abfrage eines anderen Formats aufgerufen wird.

Wenn ein Objekt keine Annotationen hat, sollte __annotate__ vorzugsweise auf None gesetzt werden (es kann nicht gelöscht werden), anstatt auf eine Funktion gesetzt zu werden, die ein leeres Dictionary zurückgibt.

Wenn der Python-Compiler ein Objekt mit Annotationen kompiliert, kompiliert er gleichzeitig die entsprechende Annotationsfunktion. Diese Funktion, die mit dem einzigen Positionsargument inspect.VALUE aufgerufen wird, berechnet und gibt das Annotations-Dictionary zurück, wie es für dieses Objekt definiert ist. Der Python-Compiler und die Laufzeitumgebung arbeiten zusammen, um sicherzustellen, dass die Funktion an die entsprechenden Namensräume gebunden ist.

  • Für Funktionen und Klassen ist das globale Dictionary das Modul, in dem das Objekt definiert wurde. Wenn das Objekt selbst ein Modul ist, ist sein globales Dictionary sein eigenes Dictionary.
  • Für Methoden von Klassen und für Klassen ist das lokale Dictionary das Klassendictionary.
  • Wenn die Annotationen freie Variablen referenzieren, ist der Closure das entsprechende Closure-Tupel, das Zellen für freie Variablen enthält.

Zweitens verlangt dieses PEP, dass die vorhandene __annotations__ ein „Daten-Deskriptor“ ist, der alle drei Aktionen implementiert: get, set und delete. __annotations__ muss auch einen eigenen internen Speicher haben, den es zum Zwischenspeichern einer Referenz auf das Annotations-Dictionary verwendet.

  • Klassen- und Objektmodule müssen das Annotations-Dictionary in ihrem __dict__ unter dem Schlüssel __annotations__ zwischenspeichern. Dies ist aus Gründen der Abwärtskompatibilität erforderlich.
  • Für Funktionsobjekte ist der Speicher für den Cache des Annotations-Dictionaries ein Implementierungsdetail. Er ist vorzugsweise intern zum Funktionsobjekt und in Python nicht sichtbar.

Dieses PEP definiert Semantiken für die Interaktion von __annotations__ und __annotate__ für alle drei Typen, die sie implementieren. In den folgenden Beispielen repräsentiert fn eine Funktion, cls eine Klasse, mod ein Modul und o ein Objekt eines dieser drei Typen.

  • Wenn o.__annotations__ ausgewertet wird, der interne Speicher für o.__annotations__ nicht gesetzt ist und o.__annotate__ auf ein aufrufbares Objekt gesetzt ist, ruft der Getter für o.__annotations__ o.__annotate__(1) auf, zwischenspeichert das Ergebnis in seinem internen Speicher und gibt das Ergebnis zurück.
    • Um eine mehrfach aufgetretene Frage explizit zu klären: Dieser o.__annotations__-Cache ist der einzige in diesem PEP definierte Caching-Mechanismus. Es gibt keine anderen in diesem PEP definierten Caching-Mechanismen. Die von Python-Compiler generierten __annotate__-Funktionen cachen explizit keine der von ihnen berechneten Werte.
  • Das Setzen von o.__annotate__ auf ein aufrufbares Objekt macht das zwischengespeicherte Annotations-Dictionary ungültig.
  • Das Setzen von o.__annotate__ auf None hat keine Auswirkungen auf das zwischengespeicherte Annotations-Dictionary.
  • Das Löschen von o.__annotate__ löst TypeError aus. __annotate__ muss immer gesetzt sein; dies verhindert, dass nicht annotierte Unterklassen die __annotate__-Methode ihrer Basisklassen erben.
  • Das Setzen von o.__annotations__ auf einen gültigen Wert setzt automatisch o.__annotate__ auf None.
    • Das Setzen von cls.__annotations__ oder mod.__annotations__ auf None verhält sich ansonsten wie jedes andere Attribut; das Attribut wird auf None gesetzt.
    • Das Setzen von fn.__annotations__ auf None macht das zwischengespeicherte Annotations-Dictionary ungültig. Wenn fn.__annotations__ keinen zwischengespeicherten Annotationswert hat und fn.__annotate__ None ist, erstellt, zwischenspeichert und gibt der Daten-Deskriptor fn.__annotations__ ein neues leeres Dictionary zurück. (Dies dient der Abwärtskompatibilität mit den Semantiken von PEP 3107.)

Änderungen an der zulässigen Annotationssyntax

__annotate__ verzögert nun die Auswertung von Annotationen, bis __annotations__ in Zukunft referenziert wird. Es bedeutet auch, dass Annotationen in einer neuen Funktion ausgewertet werden, anstatt im ursprünglichen Kontext, in dem das Objekt, auf dem sie definiert wurden, gebunden war. Es gibt vier Operatoren mit signifikanten Laufzeit-Nebeneffekten, die in den Standardsemantiken erlaubt waren, aber ungültig sind, wenn from __future__ import annotations aktiv ist, und ungültig sein müssen, wenn dieses PEP aktiv ist.

  • :=
  • yield
  • yield from
  • await

Änderungen an inspect.get_annotations und typing.get_type_hints

(Dieses PEP verweist häufig auf diese beiden Funktionen. Zukünftig wird es auf sie als „Hilfsfunktionen“ verweisen, da sie Benutzercode bei der Arbeit mit Annotationen unterstützen.)

Diese beiden Funktionen extrahieren und geben die Annotationen aus einem Objekt zurück. inspect.get_annotations gibt die Annotationen unverändert zurück; zur Erleichterung von statischen Typisierungsbenutzern nimmt typing.get_type_hints einige Änderungen an den Annotationen vor, bevor sie zurückgegeben werden.

Dieses PEP fügt diesen beiden Funktionen einen neuen schlüsselwortbasierten Parameter hinzu: format. format gibt an, in welchem Format die Werte im Annotations-Dictionary zurückgegeben werden sollen. Der format-Parameter dieser beiden Funktionen akzeptiert dieselben Werte wie der format-Parameter der oben definierten Magischen Methode __annotate__; diese format-Parameter haben jedoch auch einen Standardwert von inspect.VALUE.

Wenn __annotations__ oder __annotate__ auf einem Objekt aktualisiert werden, ist das andere dieser beiden Attribute veraltet und sollte ebenfalls entweder aktualisiert oder gelöscht werden (im Falle von __annotate__, das nicht gelöscht werden kann, auf None gesetzt). Im Allgemeinen stellen die im vorherigen Abschnitt festgelegten Semantiken sicher, dass dies automatisch geschieht. Es gibt jedoch einen Fall, der praktisch nicht automatisch behandelt werden kann: wenn das von o.__annotations__ zwischengespeicherte Dictionary selbst modifiziert wird oder wenn mutable Werte innerhalb dieses Dictionaries modifiziert werden.

Da dies nicht im Code behandelt werden kann, muss es in der Dokumentation behandelt werden. Dieses PEP schlägt vor, die Dokumentation für inspect.get_annotations (und analog für typing.get_type_hints) wie folgt zu ändern:

Wenn Sie das __annotations__-Dictionary eines Objekts direkt modifizieren, werden diese Änderungen standardmäßig möglicherweise nicht im Dictionary reflektiert, das von inspect.get_annotations bei Abfrage des SOURCE- oder FORWARDREF-Formats für dieses Objekt zurückgegeben wird. Anstatt das __annotations__-Dictionary direkt zu ändern, sollten Sie die Methode __annotate__ dieses Objekts durch eine Funktion ersetzen, die das Annotations-Dictionary mit den gewünschten Werten berechnet. Andernfalls ist es am besten, die Methode __annotate__ des Objekts auf None zu überschreiben, um zu verhindern, dass inspect.get_annotations veraltete Ergebnisse für die Formate SOURCE und FORWARDREF generiert.

Der stringizer und die fake globals Umgebung

Wie ursprünglich vorgeschlagen, unterstützte dieses PEP viele Anwendungsfälle für Laufzeitannotationen und viele Anwendungsfälle für statische Typen. Aber das war unzureichend – dieses PEP konnte nicht akzeptiert werden, bis es alle bestehenden Anwendungsfälle erfüllte. Dies wurde zu einem langjährigen Blockierungsfaktor für dieses PEP, bis Carl Meyer die „Stringizer“ und die „Fake Globals“-Umgebung wie unten beschrieben vorschlug. Diese Techniken ermöglichen es diesem PEP, sowohl die Formate FORWARDREF als auch SOURCE zu unterstützen und damit alle verbleibenden Anwendungsfälle bestens zu erfüllen.

Kurz gesagt, diese Technik beinhaltet die Ausführung einer von Python-Compiler generierten __annotate__-Funktion in einer exotischen Laufzeitumgebung. Ihr normales globals-Dictionary wird durch ein „Fake Globals“-Dictionary ersetzt. Ein „Fake Globals“-Dictionary ist ein Dictionary mit einem wichtigen Unterschied: Jedes Mal, wenn Sie einen Schlüssel daraus „holen“, der nicht zugeordnet ist, wird ein neuer Wert für diesen Schlüssel erstellt, zwischengespeichert und zurückgegeben (gemäß dem Rückruf __missing__ für ein Dictionary). Dieser Wert ist eine Instanz eines neuartigen Typs, der als „Stringizer“ bezeichnet wird.

Ein „Stringizer“ ist eine Python-Klasse mit sehr ungewöhnlichem Verhalten. Jeder Stringizer wird mit seinem „Wert“ initialisiert, der zunächst der Name des fehlenden Schlüssels im „Fake Globals“-Dictionary ist. Der Stringizer implementiert dann jede Python-„Dunder“-Methode, die zur Implementierung von Operatoren verwendet wird, und der von dieser Methode zurückgegebene Wert ist ein neuer Stringizer, dessen Wert eine Textdarstellung dieser Operation ist.

Wenn diese Stringizer in Ausdrücken verwendet werden, ist das Ergebnis des Ausdrucks ein neuer Stringizer, dessen Name diesen Ausdruck textuell darstellt. Zum Beispiel, sagen wir, Sie haben eine Variable f, die eine Referenz auf einen Stringizer ist, der mit dem Wert 'f' initialisiert wurde. Hier sind einige Beispiele für Operationen, die Sie auf f ausführen könnten, und die Werte, die sie zurückgeben würden:

>>> f
Stringizer('f')
>>> f + 3
Stringizer('f + 3')
>> f["key"]
Stringizer('f["key"]')

Alles zusammengeführt: Wenn wir eine von Python generierte __annotate__-Funktion ausführen, aber ihre Globals durch ein „Fake Globals“-Dictionary ersetzen, werden alle undefinierten Symbole, auf die sie verweist, durch Stringizer-Proxy-Objekte ersetzt, die diese Symbole darstellen, und alle Operationen, die auf diesen Proxys ausgeführt werden, ergeben wiederum Proxys, die diesen Ausdruck darstellen. Dies ermöglicht __annotate__, abzuschließen und ein Annotations-Dictionary zurückzugeben, wobei Stringizer-Instanzen Namen und ganze Ausdrücke ersetzen, die sonst nicht hätten ausgewertet werden können.

In der Praxis wird die „Stringizer“-Funktionalität im ForwardRef-Objekt implementiert, das derzeit im Modul typing definiert ist. ForwardRef wird erweitert, um die gesamte Stringizer-Funktionalität zu implementieren; es wird auch erweitert, um die Auswertung des von ihm enthaltenen Strings zu unterstützen, um den tatsächlichen Wert zu erzeugen (vorausgesetzt, alle referenzierten Symbole sind definiert). Das bedeutet, dass das ForwardRef-Objekt Referenzen auf die entsprechenden „globals“, „locals“ und sogar „closure“-Informationen behält, die zur Auswertung des Ausdrucks benötigt werden.

Diese Technik ist der Kern dessen, wie inspect.get_annotations die Formate FORWARDREF und SOURCE unterstützt. Anfangs ruft inspect.get_annotations die __annotate__-Methode des Objekts auf und fordert das gewünschte Format an. Wenn dies NotImplementedError auslöst, erstellt inspect.get_annotations eine „Fake Globals“-Umgebung und ruft dann die __annotate__-Methode des Objekts auf.

  • inspect.get_annotations erzeugt das SOURCE-Format, indem es ein neues leeres „Fake Globals“-Dictionary erstellt, es an die __annotate__-Methode des Objekts bindet, es mit der Aufforderung nach VALUE-Format aufruft und dann den String-„Wert“ aus jedem ForwardRef-Objekt im resultierenden Dictionary extrahiert.
  • inspect.get_annotations erzeugt das FORWARDREF-Format, indem es ein neues leeres „Fake Globals“-Dictionary erstellt, es mit dem aktuellen Inhalt des Globals-Dictionaries der __annotate__-Methode vorab befüllt, das „Fake Globals“-Dictionary an die __annotate__-Methode des Objekts bindet, es mit der Aufforderung nach VALUE-Format aufruft und das Ergebnis zurückgibt.

Die gesamte Technik funktioniert, weil die vom Compiler generierten __annotate__-Funktionen von Python selbst gesteuert werden und einfach und vorhersehbar sind. Sie sind im Wesentlichen eine einzige return-Anweisung, die das Annotations-Dictionary berechnet und zurückgibt. Da die meisten Operationen, die zur Berechnung einer Annotation benötigt werden, in Python mit Dunder-Methoden implementiert sind und der Stringizer alle relevanten Dunder-Methoden unterstützt, ist dieser Ansatz eine zuverlässige, praktische Lösung.

Es ist jedoch nicht ratsam, diese Technik mit einer beliebigen __annotate__-Methode zu versuchen. Dieses PEP geht davon aus, dass Drittanbieterbibliotheken ihre eigenen __annotate__-Methoden implementieren könnten, und diese Funktionen würden bei der Ausführung in dieser „Fake Globals“-Umgebung fast sicher falsch funktionieren. Aus diesem Grund weist dieses PEP ein Flag auf Code-Objekten zu, eines der ungenutzten Bits in co_flags, das bedeutet „Dieses Code-Objekt kann in einer ‚Fake Globals‘-Umgebung ausgeführt werden.“ Dies macht die „Fake Globals“-Umgebung strikt opt-in, und es wird erwartet, dass nur von Python-Compiler generierte __annotate__-Methoden es setzen.

Die Schwäche dieser Technik liegt im Umgang mit Operatoren, die nicht direkt auf Dunder-Methoden eines Objekts abgebildet werden. Dies sind alle Operatoren, die eine Art von Flusskontrolle implementieren, entweder Verzweigung oder Iteration.

  • Short-Circuiting or
  • Short-Circuiting and
  • Ternärer Operator (der if / then Operator)
  • Generator-Ausdrücke
  • Listen-/Dict-/Set-Comprehensions
  • Iterierbare Entpackung

Als Faustregel werden diese Techniken nicht in Annotationen verwendet, daher stellt dies in der Praxis kein Problem dar. Die kürzlich erfolgte Hinzufügung von TypeVarTuple zu Python verwendet jedoch iterierbare Entpackung. Die beteiligten Dunder-Methoden (__iter__ und __next__) erlauben keine Unterscheidung zwischen Iterationsanwendungsfällen; um korrekt zu erkennen, welcher Anwendungsfall beteiligt war, wären reine „Fake Globals“ und ein „Stringizer“ nicht ausreichend; dies würde einen benutzerdefinierten Bytecode-Interpreter erfordern, der speziell auf die Erzeugung von SOURCE- und FORWARDREF-Formaten ausgelegt ist.

Glücklicherweise gibt es eine Abkürzung, die gut funktionieren wird: Der Stringizer nimmt einfach an, dass er, wenn seine Iterations-Dunder-Methoden aufgerufen werden, im Dienste der Iterator-Entpackung steht, die von TypeVarTuple durchgeführt wird. Er wird dieses Verhalten hartkodieren. Das bedeutet, dass keine andere Technik, die Iteration verwendet, funktionieren wird, aber in der Praxis wird dies reale Anwendungsfälle nicht beeinträchtigen.

Schließlich ist zu beachten, dass die „Fake Globals“-Umgebung auch die Erstellung eines passenden „Fake Locals“-Dictionaries erfordert, das für das FORWARDREF-Format mit dem relevanten Locals-Dictionary vorab befüllt wird. Die „Fake Globals“-Umgebung muss auch einen Fake „Closure“ erstellen, ein Tupel von ForwardRef-Objekten, die mit den Namen der freien Variablen, auf die die __annotate__-Methode verweist, vorab erstellt wurden.

ForwardRef-Proxys, die aus __annotate__-Methoden erstellt werden, die auf freie Variablen verweisen, ordnen die Namen und Closure-Werte dieser freien Variablen dem Locals-Dictionary zu, um sicherzustellen, dass eval die richtigen Werte für diese Namen verwendet.

Vom Compiler generierte __annotate__ Funktionen

Wie im vorherigen Abschnitt erwähnt, sind die vom Compiler generierten __annotate__-Funktionen einfach. Sie sind hauptsächlich eine einzige return-Anweisung, die das Annotations-Dictionary berechnet und zurückgibt.

Das Protokoll für inspect.get_annotations, um entweder das FORWARDREF- oder das SOURCE-Format anzufordern, erfordert jedoch, dass zuerst die __annotate__-Methode aufgefordert wird, es zu produzieren. Von Python-Compiler generierte __annotate__-Methoden unterstützen keines dieser Formate und lösen NotImplementedError() aus.

Von Drittanbietern erstellte __annotate__ Funktionen

Drittanbieterklassen und -funktionen müssen wahrscheinlich ihre eigenen __annotate__-Methoden implementieren, damit nachgelagerte Benutzer dieser Objekte Annotationen voll ausnutzen können. Insbesondere Wrapper müssen wahrscheinlich die Annotations-Dictionaries transformieren, die vom umwickelten Objekt produziert werden: das Dictionary auf irgendeine Weise hinzufügen, entfernen oder modifizieren.

Die meiste Zeit implementieren Drittanbietercode ihre __annotate__-Methoden, indem sie inspect.get_annotations für ein bestehendes Upstream-Objekt aufrufen. Zum Beispiel werden Wrapper wahrscheinlich das Annotations-Dictionary für ihr umwickeltes Objekt in dem Format anfordern, das von ihnen angefordert wurde, dann das zurückgegebene Annotations-Dictionary entsprechend modifizieren und es zurückgeben. Dies ermöglicht Drittanbietercode, die „Fake Globals“-Technik zu nutzen, ohne sie verstehen oder daran teilnehmen zu müssen.

Drittanbieterbibliotheken, die sowohl Prä- als auch Post-PEP-649-Versionen von Python unterstützen, müssen ihre eigenen Best Practices entwickeln, wie sie beide unterstützen können. Ein sinnvoller Ansatz wäre, dass ihr Wrapper immer __annotate__ unterstützt, dann VALUE-Format anfordert und das Ergebnis als __annotations__ auf ihrem Wrapper-Objekt speichert. Dies würde die Prä-649-Python-Semantiken unterstützen und wäre vorwärtskompatibel mit Post-649-Semantiken.

Pseudocode

Hier ist Pseudocode auf hoher Ebene für inspect.get_annotations:

def get_annotations(o, format):
    if format == VALUE:
        return dict(o.__annotations__)

    if format == FORWARDREF:
        try:
            return dict(o.__annotations__)
        except NameError:
            pass

    if not hasattr(o.__annotate__):
        return {}

    c_a = o.__annotate__
    try:
        return c_a(format)
    except NotImplementedError:
        if not can_be_called_with_fake_globals(c_a):
            return {}
        c_a_with_fake_globals = make_fake_globals_version(c_a, format)
        return c_a_with_fake_globals(VALUE)

Hier ist, wie eine von Python-Compiler generierte __annotate__-Methode aussehen könnte, wenn sie in Python geschrieben wäre:

def __annotate__(self, format):
    if format != 1:
        raise NotImplementedError()
    return { ... }

Hier ist, wie eine Drittanbieter-Wrapper-Klasse __annotate__ implementieren könnte. In diesem Beispiel funktioniert der Wrapper wie functools.partial und bindet einen Parameter des umwickelten aufrufbaren Objekts vorab, der der Einfachheit halber arg heißen muss:

def __annotate__(self, format):
    ann = inspect.get_annotations(self.wrapped_fn, format)
    if 'arg' in ann:
        del ann['arg']
    return ann

Andere Modifikationen der Python-Laufzeitumgebung

Dieses PEP schreibt nicht genau vor, wie es implementiert werden soll; das bleibt den Betreuern der Sprachimplementierung überlassen. Die beste Implementierung dieses PEP könnte jedoch das Hinzufügen zusätzlicher Informationen zu bestehenden Python-Objekten erfordern, was durch die Akzeptanz dieses PEP implizit geduldet wird.

Es könnte beispielsweise notwendig sein, Klassenobjekten ein __globals__-Attribut hinzuzufügen, damit die __annotate__-Funktion für diese Klasse verzögert, nur bei Bedarf, gebunden werden kann. Außerdem können __annotate__-Funktionen, die auf Methoden definiert sind, die in einer Klasse definiert sind, eine Referenz auf das __dict__ der Klasse behalten müssen, um Namen, die in dieser Klasse gebunden sind, korrekt auszuwerten. Es wird erwartet, dass die CPython-Implementierung dieses PEP beide neuen Attribute enthalten wird.

Alle solchen neuen Informationen, die bestehenden Python-Objekten hinzugefügt werden, sollten mit „Dunder“-Attributen erfolgen, da sie natürlich Implementierungsdetails sein werden.

Interaktive REPL-Shell

Die in diesem PEP festgelegten Semantiken gelten auch bei der Ausführung von Code in der interaktiven REPL-Shell von Python, mit Ausnahme von Modul-Annotationen im interaktiven Modul (__main__) selbst. Da dieses Modul nie „abgeschlossen“ wird, gibt es keinen spezifischen Zeitpunkt, an dem wir die __annotate__-Funktion kompilieren können.

Der Einfachheit halber verzichten wir in diesem Fall auf verzögerte Auswertung. Modulweite Annotationen in der REPL-Shell funktionieren weiterhin genau wie bei „Stock-Semantiken“, werden sofort ausgewertet und das Ergebnis direkt in das __annotations__-Dictionary geschrieben.

Annotationen zu lokalen Variablen innerhalb von Funktionen

Python unterstützt Syntax für lokale Variablenannotationen innerhalb von Funktionen. Diese Annotationen haben jedoch keine Laufzeitwirkung – sie werden zur Kompilierungszeit verworfen. Daher muss dieses PEP nichts tun, um sie zu unterstützen, genauso wenig wie die Stock-Semantiken und PEP 563.

Prototyp

Die ursprüngliche Prototypimplementierung dieses PEP finden Sie hier:

https://github.com/larryhastings/co_annotations/

Zum Zeitpunkt des Schreibens ist die Implementierung stark veraltet; sie basiert auf Python 3.10 und implementiert die Semantiken des ersten Entwurfs dieses PEP von Anfang 2021. Sie wird in Kürze aktualisiert.

Performance-Vergleich

Die Leistung mit diesem PEP ist im Allgemeinen günstig. Es sind vier Szenarien zu berücksichtigen:

  • die Laufzeitkosten, wenn Annotationen nicht definiert sind,
  • die Laufzeitkosten, wenn Annotationen definiert, aber nicht referenziert sind, und
  • die Laufzeitkosten, wenn Annotationen definiert und als Objekte referenziert werden.
  • die Laufzeitkosten, wenn Annotationen definiert und als Strings referenziert werden.

Wir werden jedes dieser Szenarien im Kontext aller drei Semantiken für Annotationen untersuchen: Stock, PEP 563 und dieses PEP.

Wenn keine Annotationen vorhanden sind, haben alle drei Semantiken die gleichen Laufzeitkosten: null. Es wird kein Annotations-Dictionary erstellt und kein Code dafür generiert. Dies erfordert keine Laufzeitverarbeitungszeit und verbraucht keinen Speicher.

Wenn Annotationen definiert, aber nicht referenziert sind, sind die Laufzeitkosten von Python mit diesem PEP ungefähr gleich wie bei PEP 563 und besser als bei Stock. Die Einzelheiten hängen vom annotierten Objekt ab:

  • Mit Stock-Semantiken wird das Annotations-Dictionary immer erstellt und als Attribut des annotierten Objekts gesetzt.
  • In der Semantik von PEP 563 wird für Funktionsobjekte eine vorkompilierte Konstante (ein speziell konstruiertes Tupel) als Attribut der Funktion gesetzt. Für Klassen- und Modulobjekte wird das Annotations-Dictionary immer erstellt und als Attribut der Klasse oder des Moduls gesetzt.
  • Mit diesem PEP wird ein einzelnes Objekt als Attribut des annotierten Objekts gesetzt. Meistens ist dieses Objekt eine Konstante (ein Code-Objekt), aber wenn die Annotationen einen Klassen-Namensraum oder einen Closure erfordern, ist dieses Objekt ein Tupel, das zur Bindungszeit konstruiert wird.

Wenn Annotationen sowohl definiert als auch als Objekte referenziert werden, sollte der Code, der dieses PEP verwendet, viel schneller sein als PEP 563 und so schnell oder schneller sein als Stock. PEP 563-Semantiken erfordern die Invokation von eval() für jeden Wert innerhalb eines Annotations-Dictionaries, was enorm langsam ist. Und die Implementierung dieses PEP generiert messbar effizienteren Bytecode für Klassen- und Modulannotationen als Stock-Semantiken; für Funktionsannotationen sollten dieses PEP und Stock-Semantiken ungefähr gleich schnell sein.

Der einzige Fall, in dem dieses PEP merklich langsamer sein wird als PEP 563, ist, wenn Annotationen als Strings angefordert werden; es ist schwer zu übertreffen, dass sie „bereits Strings sind“. Stringifizierte Annotationen sind jedoch für Anwendungsfälle der Online-Dokumentation gedacht, bei denen die Leistung wahrscheinlich keine entscheidende Rolle spielt.

Der Speicherverbrauch sollte ebenfalls in allen drei Szenarien über alle drei semantischen Kontexte hinweg vergleichbar sein. Im ersten und dritten Szenario sollte der Speicherverbrauch in allen Fällen ungefähr gleich sein. Im zweiten Szenario, wenn Annotationen definiert, aber nicht referenziert sind, bedeutet die Verwendung der Semantiken dieses PEP, dass die Funktion/Klasse/das Modul ein ungenutztes Code-Objekt speichert (möglicherweise an ein ungenutztes Funktions-Objekt gebunden); bei den anderen beiden Semantiken speichern sie ein ungenutztes Dictionary oder ein konstantes Tupel.

Abwärtskompatibilität

Abwärtskompatibilität mit Standardsemantik

Dieses PEP bewahrt fast alle bestehenden Verhaltensweisen von Annotationen aus den Stock-Semantiken.

  • Das Format des Annotations-Dictionaries, das im Attribut __annotations__ gespeichert ist, bleibt unverändert. Annotations-Dictionaries enthalten echte Werte, keine Strings gemäß PEP 563.
  • Annotations-Dictionaries sind veränderbar, und alle Änderungen daran bleiben erhalten.
  • Das Attribut __annotations__ kann explizit gesetzt werden, und jeder gültige Wert, der auf diese Weise gesetzt wird, bleibt erhalten.
  • Das Attribut __annotations__ kann mit der del-Anweisung gelöscht werden.

Der Großteil des Codes, der mit Stock-Semantiken funktioniert, sollte auch bei aktivem PEP weiterhin ohne Änderungen funktionieren. Es gibt jedoch Ausnahmen, wie folgt.

Erstens gibt es ein bekanntes Idiom zum Zugriff auf Klassenannotationen, das bei aktivem PEP möglicherweise nicht korrekt funktioniert. Die ursprüngliche Implementierung von Klassenannotationen hatte, was nur als Fehler bezeichnet werden kann: Wenn eine Klasse keine eigenen Annotationen definierte, aber eine ihrer Basisklassen Annotationen definierte, „erbte“ die Klasse diese Annotationen. Dieses Verhalten war nie wünschenswert, daher fand Benutzercode eine Umgehung: Anstatt direkt über cls.__annotations__ auf die Annotationen der Klasse zuzugreifen, griffen die Code auf die Annotationen der Klasse über ihr Dictionary zu, wie in cls.__dict__.get("__annotations__", {}). Dieses Idiom funktionierte, da Klassen ihre Annotationen in ihrem __dict__ speicherten und der Zugriff darauf die Suche in den Basisklassen vermied. Die Technik beruhte auf Implementierungsdetails von CPython, so dass sie nie unterstütztes Verhalten war – obwohl sie notwendig war. Wenn dieses PEP jedoch aktiv ist, kann eine Klasse Annotationen definiert haben, aber __annotate__ noch nicht aufgerufen und das Ergebnis zwischengespeichert haben, in welchem Fall dieser Ansatz fälschlicherweise davon ausgehen würde, dass die Klasse keine Annotationen hat. In jedem Fall wurde der Fehler ab Python 3.10 behoben, und das Idiom sollte nicht mehr verwendet werden. Ebenfalls ab Python 3.10 gibt es ein Annotations HOWTO, das Best Practices für die Arbeit mit Annotationen definiert; Code, der diesen Richtlinien folgt, funktioniert auch bei aktivem PEP korrekt, da er vorschlägt, je nach Python-Version unterschiedliche Ansätze für den Abruf von Annotationen von Klassenobjekten zu verwenden.

Da die Verzögerung der Auswertung von Annotationen bis zu ihrer Introspektion die Semantik der Sprache verändert, ist sie aus der Sprache heraus beobachtbar. Daher ist es möglich, Code zu schreiben, der sich unterschiedlich verhält, je nachdem, ob Annotationen zur Bindungszeit oder zur Zugriffszeit ausgewertet werden, z. B.:

mytype = str
def foo(a:mytype): pass
mytype = int
print(foo.__annotations__['a'])

Dies gibt <class 'str'> mit Stock-Semantiken und <class 'int'> aus, wenn dieses PEP aktiv ist. Dies ist daher eine abwärtsinkompatible Änderung. Dieses Beispiel ist jedoch schlechter Programmierstil, sodass diese Änderung akzeptabel erscheint.

Es gibt zwei seltene Interaktionen mit Klassen- und Modulannotationen, die mit Stock-Semantiken funktionieren, aber bei aktivem PEP nicht mehr funktionieren würden. Diese beiden Interaktionen müssten verboten werden. Die gute Nachricht ist, dass keine davon häufig ist und keine als gute Praxis gilt. Tatsächlich sind sie selten außerhalb der eigenen Regressionstestsuite von Python zu finden. Sie sind:

  • Code, der Annotationen auf Modul- oder Klassenattributen innerhalb einer beliebigen Art von Kontrollflussanweisung setzt. Es ist derzeit möglich, Modul- und Klassenattribute mit Annotationen innerhalb einer if- oder try-Anweisung zu setzen, und es funktioniert wie erwartet. Es ist untragbar, dieses Verhalten bei aktivem PEP zu unterstützen.
  • Code im Modul- oder Klassenumfang, der das lokale __annotations__-Dictionary direkt referenziert oder modifiziert. Derzeit erstellt der generierte Code beim Setzen von Annotationen auf Modul- oder Klassenattributen einfach ein lokales __annotations__-Dictionary und fügt dann bei Bedarf Zuordnungen hinzu. Es ist möglich, dass Benutzercode dieses Dictionary direkt modifiziert, obwohl dies keine beabsichtigte Funktion zu sein scheint. Obwohl es möglich wäre, dies nach dem Aktivieren dieses PEP zu unterstützen, wären die Semantiken wahrscheinlich überraschend und würden niemanden glücklich machen.

Beachten Sie, dass dies beides auch Schmerzpunkte für statische Typenprüfer sind und von diesen Tools nicht unterstützt werden. Es scheint vernünftig zu erklären, dass beide zumindest nicht unterstützt werden und ihre Verwendung zu undefiniertem Verhalten führt. Es könnte sich lohnen, kleine Anstrengungen zu unternehmen, um sie explizit mit Kompilierungszeitprüfungen zu verbieten.

Schließlich sollten bei aktiver PEP die Annotationswerte nicht den ternären Operator if / else verwenden. Dies funktioniert zwar korrekt beim Zugriff auf o.__annotations__ oder bei der Anforderung von inspect.VALUE aus einer Hilfsfunktion, aber der boolesche Ausdruck berechnet möglicherweise nicht korrekt mit inspect.FORWARDREF, wenn einige Namen definiert sind, und wäre weit weniger korrekt mit inspect.SOURCE.

Abwärtskompatibilität mit PEP 563 Semantik

PEP 563 änderte die Semantik von Annotationen. Wenn seine Semantik aktiv ist, müssen Annotationen davon ausgehen, dass sie im Geltungsbereich auf **Modulebene** oder auf **Klassenebene** ausgewertet werden. Sie dürfen lokale Variablen in der aktuellen oder einer übergeordneten Funktion nicht mehr direkt referenzieren. Diese PEP hebt diese Einschränkung auf, und Annotationen dürfen jede lokale Variable referenzieren.

PEP 563 erfordert die Verwendung von eval (oder einer Hilfsfunktion wie typing.get_type_hints oder inspect.get_annotations, die eval für Sie verwendet), um stringifizierte Annotationen in ihre "echten" Werte umzuwandeln. Vorhandener Code, der stringifizierte Annotationen aktiviert und eval() direkt aufruft, um die Zeichenfolgen zurück in echte Werte umzuwandeln, kann den eval()-Aufruf einfach entfernen. Vorhandener Code, der eine Hilfsfunktion verwendet, würde unverändert weiter funktionieren, obwohl die Verwendung dieser Funktionen optional werden könnte.

Benutzer von statischer Typisierung haben oft Module, die nur inerte Typ-Hint-Definitionen enthalten – aber keinen Live-Code. Diese Module werden nur zur Laufzeit der statischen Typüberprüfung benötigt; sie werden zur Laufzeit nicht verwendet. Aber unter den Standardsemantiken müssen diese Module importiert werden, damit die Laufzeit die Annotationen auswerten und berechnen kann. In der Zwischenzeit verursachten diese Module oft zirkuläre Importprobleme, die schwer oder sogar unmöglich zu lösen waren. PEP 563 erlaubte Benutzern, diese zirkulären Importprobleme zu lösen, indem sie zwei Dinge taten. Erstens aktivierten sie PEP 563 in ihren Modulen, was bedeutete, dass Annotationen konstante Zeichenfolgen waren und die echten Symbole nicht definiert sein mussten, damit die Annotationen berechenbar waren. Zweitens erlaubte dies den Benutzern, die problematischen Module nur in einem if typing.TYPE_CHECKING-Block zu importieren. Dies erlaubte den statischen Typüberprüfern, die Module und die darin enthaltenen Typdefinitionen zu importieren, aber sie würden zur Laufzeit nicht importiert werden. Bisher wird dieser Ansatz unverändert funktionieren, wenn diese PEP aktiv ist; if typing.TYPE_CHECKING ist ein unterstütztes Verhalten.

Einige Codebasen untersuchten ihre Annotationen tatsächlich zur Laufzeit, selbst wenn sie die if typing.TYPE_CHECKING-Technik verwendeten und Definitionen, die in ihren Annotationen verwendet wurden, nicht importierten. Diese Codebasen untersuchten die Annotationszeichenfolgen, **ohne sie auszuwerten**, und verließen sich stattdessen auf Identitätsprüfungen oder einfache lexikalische Analyse der Zeichenfolgen.

Diese PEP unterstützt auch diese Techniken. Benutzer müssen jedoch ihren Code dafür portieren. Erstens muss der Benutzercode inspect.get_annotations oder typing.get_type_hints verwenden, um auf die Annotationen zuzugreifen; sie können nicht einfach auf das __annotations__-Attribut ihres Objekts zugreifen. Zweitens müssen sie entweder inspect.FORWARDREF oder inspect.SOURCE für das format angeben, wenn sie diese Funktion aufrufen. Dies bedeutet, dass die Hilfsfunktion Annotationen-Dictionaries erfolgreich erstellen kann, auch wenn nicht alle Symbole definiert sind. Code, der stringifizierte Annotationen erwartet, sollte mit inspect.SOURCE-formatierten Annotationen-Dictionaries unverändert funktionieren; Benutzer sollten jedoch erwägen, zu inspect.FORWARDREF zu wechseln, da dies ihre Analyse erleichtern könnte.

Ähnlich erlaubte PEP 563 die Verwendung von Klassendekoratoren für annotierte Klassen auf eine Weise, die zuvor nicht möglich war. Einige Klassendekoratoren (z. B. dataclasses) untersuchen die Annotationen der Klasse. Da Klassendekoratoren, die die @-Dekoratorsyntax verwenden, vor der Bindung des Klassennamens ausgeführt werden, können sie zu unlösbaren Problemen mit zirkulären Definitionen führen. Wenn Sie Attribute einer Klasse mit Verweisen auf die Klasse selbst annotieren oder Attribute in mehreren Klassen mit zirkulären Verweisen aufeinander annotieren, können Sie diese Klassen nicht mit der @-Dekoratorsyntax mit Dekoratoren dekorieren, die die Annotationen untersuchen. PEP 563 erlaubte dies, solange die Dekoratoren die Zeichenfolgen lexikalisch untersuchten und eval nicht verwendeten, um sie auszuwerten (oder den NameError mit weiteren Workarounds behandelten). Wenn diese PEP aktiv ist, können Dekoratoren das Annotationen-Dictionary im inspect.SOURCE- oder inspect.FORWARDREF-Format unter Verwendung der Hilfsfunktionen berechnen. Dies ermöglicht es ihnen, Annotationen zu analysieren, die undefinierte Symbole enthalten, in dem Format, das sie bevorzugen.

Frühe Anwender von PEP 563 stellten fest, dass "stringifizierte" Annotationen für automatisch generierte Dokumentationen nützlich waren. Benutzer experimentierten mit diesem Anwendungsfall, und Pythons pydoc hat ein gewisses Interesse an dieser Technik gezeigt. Diese PEP unterstützt diesen Anwendungsfall; der Code, der die Dokumentation generiert, muss aktualisiert werden, um eine Hilfsfunktion zu verwenden, um die Annotationen im inspect.SOURCE-Format abzurufen.

Schließlich gelten die Warnungen zur Verwendung des ternären Operators if / else in Annotationen gleichermaßen für Benutzer von PEP 563. Er funktioniert derzeit für sie, könnte aber bei der Anforderung einiger Formate von den Hilfsfunktionen zu falschen Ergebnissen führen.

Wenn diese PEP angenommen wird, wird PEP 563 als veraltet eingestuft und schließlich entfernt. Um diesen Übergang für frühe Anwender von PEP 563, die nun von seiner Semantik abhängen, zu erleichtern, implementieren inspect.get_annotations und typing.get_type_hints eine spezielle Erleichterung.

Der Python-Compiler wird keine Annotationen-Codeobjekte für Objekte generieren, die in einem Modul definiert sind, in dem die PEP 563-Semantik aktiv ist, selbst wenn diese PEP angenommen wird. Daher würde die Anforderung des inspect.SOURCE-Formats von einer Hilfsfunktion unter normalen Umständen ein leeres Dictionary zurückgeben. Als Erleichterung zur Erleichterung des Übergangs, wenn die Hilfsfunktionen erkennen, dass ein Objekt in einem Modul mit aktiver PEP 563-Semantik definiert wurde und der Benutzer das inspect.SOURCE-Format anfordert, geben sie den aktuellen Wert des __annotations__-Dictionarys zurück, das in diesem Fall die stringifizierten Annotationen sein werden. Dies ermöglicht es PEP 563-Benutzern, die stringifizierte Annotationen lexikalisch analysieren, sofort zu Anforderung des inspect.SOURCE-Formats von den Hilfsfunktionen zu wechseln, was hoffentlich ihren Übergang weg von PEP 563 glätten wird.

Abgelehnte Ideen

„Speichere einfach die Strings“

Eine vorgeschlagene Idee zur Unterstützung des SOURCE-Formats war, dass der Python-Compiler den tatsächlichen Quellcode für die Annotationswerte irgendwo ausgibt und diesen bereitstellt, wenn der Benutzer SOURCE-Format anfordert.

Diese Idee wurde nicht so sehr verworfen, als dass sie als "noch nicht" kategorisiert wurde. Wir wissen bereits, dass wir das FORWARDREF-Format unterstützen müssen, und diese Technik kann mit nur wenigen Zeilen angepasst werden, um das SOURCE-Format zu unterstützen. Es gibt viele unbeantwortete Fragen zu diesem Ansatz

  • Wo würden wir die Zeichenfolgen speichern? Würden sie immer geladen werden, wenn das annotierte Objekt erstellt wurde, oder würden sie bedarfsgesteuert geladen werden? Wenn ja, wie würde das Lazy-Loading funktionieren?
  • Würde der "Quellcode" die Zeilenumbrüche und Kommentare des Originals enthalten? Würde er sämtliche Leerzeichen beibehalten, einschließlich Einrückungen und zusätzlicher Leerzeichen, die rein zur Formatierung verwendet werden?

Es ist möglich, dass wir dieses Thema in Zukunft wieder aufgreifen, wenn die Verbesserung der Wiedergabetreue von SOURCE-Werten zum ursprünglichen Quellcode als ausreichend wichtig erachtet wird.

Danksagungen

Vielen Dank an Carl Meyer, Barry Warsaw, Eric V. Smith, Mark Shannon, Jelle Zijlstra und Guido van Rossum für ihr fortlaufendes Feedback und ihre Ermutigung.

Besonderer Dank gilt mehreren Personen, die wichtige Ideen beigesteuert haben, die zu einigen der besten Aspekte dieses Vorschlags wurden.

  • Carl Meyer schlug die "Stringizer"-Technik vor, die FORWARDREF- und SOURCE-Formate ermöglichte und damit die Weiterentwicklung dieser PEP nach einem Jahr des Stillstands aufgrund scheinbar unlösbarer Probleme ermöglichte. Er schlug auch die Erleichterung für PEP 563-Benutzer vor, bei der inspect.SOURCE die stringifizierten Annotationen zurückgibt, und viele weitere Vorschläge. Carl war auch der Hauptkorrespondent in privaten E-Mail-Threads, in denen diese PEP diskutiert wurde, und war eine unermüdliche Ressource und eine Stimme der Vernunft. Diese PEP wäre fast sicher nicht angenommen worden, wenn Carl nicht dazu beigetragen hätte.
  • Mark Shannon schlug vor, das gesamte Annotationen-Dictionary innerhalb eines einzigen Codeobjekts zu erstellen und es erst bei Bedarf an eine Funktion zu binden.
  • Guido van Rossum schlug vor, dass __annotate__-Funktionen die Namenssichtbarkeitsregeln von Annotationen unter "Standardsemantik" duplizieren sollten.
  • Jelle Zijlstra trug nicht nur Feedback bei – sondern auch Code!

Referenzen


Source: https://github.com/python/peps/blob/main/peps/pep-0649.rst

Last modified: 2025-10-06 14:23:25 GMT