Annotationen Best Practices¶
- Autor:
Larry Hastings
Zugriff auf das Annotations-Dictionary eines Objekts in Python 3.10 und neuer¶
Python 3.10 fügt der Standardbibliothek eine neue Funktion hinzu: inspect.get_annotations(). In den Python-Versionen 3.10 bis 3.13 ist das Aufrufen dieser Funktion die Best Practice für den Zugriff auf das Annotations-Dictionary jedes Objekts, das Annotationen unterstützt. Diese Funktion kann auch stringifizierte Annotationen für Sie "ent-stringifizieren".
In Python 3.14 gibt es ein neues annotationlib Modul mit Funktionalität zur Arbeit mit Annotationen. Dies beinhaltet eine Funktion annotationlib.get_annotations(), die inspect.get_annotations() ersetzt.
Wenn inspect.get_annotations() aus irgendeinem Grund für Ihren Anwendungsfall nicht praktikabel ist, können Sie das Datenmember __annotations__ manuell zugreifen. Die Best Practice hierfür hat sich ebenfalls in Python 3.10 geändert: Ab Python 3.10 ist garantiert, dass o.__annotations__ *immer* auf Python-Funktionen, Klassen und Module funktioniert. Wenn Sie sicher sind, dass das zu untersuchende Objekt eines dieser drei *spezifischen* Objekte ist, können Sie einfach o.__annotations__ verwenden, um auf das Annotations-Dictionary des Objekts zuzugreifen.
Andere Arten von Aufrufbaren – zum Beispiel Aufrufbare, die von functools.partial() erstellt wurden – haben möglicherweise kein __annotations__ Attribut definiert. Beim Zugriff auf __annotations__ eines möglicherweise unbekannten Objekts ist die Best Practice in Python-Versionen 3.10 und neuer, getattr() mit drei Argumenten aufzurufen, z. B. getattr(o, '__annotations__', None).
Vor Python 3.10 würde der Zugriff auf __annotations__ auf einer Klasse, die keine Annotationen definiert, aber eine Elternklasse mit Annotationen hat, die __annotations__ der Elternklasse zurückgeben. In Python 3.10 und neuer wird die Annotation der Kindklasse stattdessen ein leeres Dictionary sein.
Zugriff auf das Annotations-Dictionary eines Objekts in Python 3.9 und älter¶
In Python 3.9 und älter ist der Zugriff auf das Annotations-Dictionary eines Objekts wesentlich komplizierter als in neueren Versionen. Das Problem ist ein Designfehler in diesen älteren Python-Versionen, insbesondere im Hinblick auf Klassenannotationen.
Die Best Practice für den Zugriff auf das Annotations-Dictionary anderer Objekte – Funktionen, andere Aufrufbare und Module – ist die gleiche wie die Best Practice für 3.10, vorausgesetzt, Sie rufen nicht inspect.get_annotations() auf: Sie sollten ein dreistufiges getattr() verwenden, um auf das __annotations__ Attribut des Objekts zuzugreifen.
Leider ist dies keine Best Practice für Klassen. Das Problem ist, dass, da __annotations__ bei Klassen optional ist und da Klassen Attribute von ihren Basisklassen erben können, der Zugriff auf das __annotations__ Attribut einer Klasse versehentlich das Annotations-Dictionary einer *Basisklasse* zurückgeben kann. Als Beispiel
class Base:
a: int = 3
b: str = 'abc'
class Derived(Base):
pass
print(Derived.__annotations__)
Dies gibt das Annotations-Dictionary von Base aus, nicht von Derived.
Ihr Code muss einen separaten Pfad haben, wenn das zu untersuchende Objekt eine Klasse ist (isinstance(o, type)). In diesem Fall basiert die Best Practice auf einem Implementierungsdetail von Python 3.9 und früher: Wenn eine Klasse Annotationen definiert hat, werden diese im __dict__ Dictionary der Klasse gespeichert. Da die Klasse möglicherweise Annotationen definiert hat oder nicht, besteht die Best Practice darin, die get() Methode des Klassen-Dictionaries aufzurufen.
Um alles zusammenzufassen, hier ist ein Beispielcode, der sicher auf das __annotations__ Attribut eines beliebigen Objekts in Python 3.9 und früher zugreift
if isinstance(o, type):
ann = o.__dict__.get('__annotations__', None)
else:
ann = getattr(o, '__annotations__', None)
Nachdem dieser Code ausgeführt wurde, sollte ann entweder ein Dictionary oder None sein. Sie werden ermutigt, den Typ von ann mit isinstance() zu überprüfen, bevor Sie ihn weiter untersuchen.
Beachten Sie, dass einige exotische oder fehlerhafte Typobjekte möglicherweise kein __dict__ Attribut haben, daher können Sie für zusätzliche Sicherheit auch getattr() verwenden, um auf __dict__ zuzugreifen.
Manuelles Ent-Stringifizieren von stringifizierten Annotationen¶
In Situationen, in denen einige Annotationen "stringifiziert" sein mögen und Sie diese Strings auswerten möchten, um die Python-Werte zu erzeugen, die sie darstellen, ist es am besten, inspect.get_annotations() aufzurufen, um diese Arbeit für Sie zu erledigen.
Wenn Sie Python 3.9 oder älter verwenden oder wenn Sie inspect.get_annotations() aus irgendeinem Grund nicht verwenden können, müssen Sie seine Logik duplizieren. Sie werden ermutigt, die Implementierung von inspect.get_annotations() in der aktuellen Python-Version zu untersuchen und einen ähnlichen Ansatz zu verfolgen.
Kurz gesagt, wenn Sie eine stringifizierte Annotation auf einem beliebigen Objekt o auswerten möchten
Wenn
oein Modul ist, verwenden Sieo.__dict__alsglobalsbeim Aufruf voneval().Wenn
oeine Klasse ist, verwenden Siesys.modules[o.__module__].__dict__alsglobalsunddict(vars(o))alslocals, beim Aufruf voneval().Wenn
oein mitfunctools.update_wrapper(),functools.wraps()oderfunctools.partial()gewickeltes Aufrufbares ist, entpacken Sie es iterativ, indem Sie entwedero.__wrapped__odero.funcwie zutreffend zugreifen, bis Sie die ursprüngliche entpackte Funktion gefunden haben.Wenn
oaufrufbar ist (aber keine Klasse), verwenden Sieo.__globals__als Globale beim Aufruf voneval().
Allerdings können nicht alle als Annotationen verwendeten Zeichenketten von eval() erfolgreich in Python-Werte umgewandelt werden. Zeichenketten können theoretisch jede gültige Zeichenkette enthalten, und in der Praxis gibt es gültige Anwendungsfälle für Typ-Hints, die das Annotieren mit Zeichenketten erfordern, die speziell *nicht* ausgewertet werden können. Zum Beispiel
PEP 604 Union-Typen mit
|, bevor die Unterstützung dafür in Python 3.10 hinzugefügt wurde.Definitionen, die zur Laufzeit nicht benötigt werden, sondern nur importiert werden, wenn
typing.TYPE_CHECKINGwahr ist.
Wenn eval() versucht, solche Werte auszuwerten, schlägt er fehl und löst eine Ausnahme aus. Daher wird bei der Gestaltung einer Bibliotheks-API, die mit Annotationen arbeitet, empfohlen, nur dann zu versuchen, Zeichenketten auszuwerten, wenn der Aufrufer dies explizit anfordert.
Best Practices für __annotations__ in jeder Python-Version¶
Sie sollten direkte Zuweisungen zum
__annotations__Member von Objekten vermeiden. Lassen Sie Python die Zuweisung von__annotations__verwalten.Wenn Sie direkt dem
__annotations__Member eines Objekts zuweisen, sollten Sie es immer auf eindictObjekt setzen.Sie sollten vermeiden,
__annotations__direkt auf irgendeinem Objekt zuzugreifen. Verwenden Sie stattdessenannotationlib.get_annotations()(Python 3.14+) oderinspect.get_annotations()(Python 3.10+).Wenn Sie direkt auf das
__annotations__Member eines Objekts zugreifen, sollten Sie sicherstellen, dass es sich um ein Dictionary handelt, bevor Sie versuchen, seinen Inhalt zu untersuchen.Sie sollten das Ändern von
__annotations__Dictionaries vermeiden.Sie sollten das Löschen des
__annotations__Attributs eines Objekts vermeiden.
__annotations__ Quirks¶
In allen Versionen von Python 3 erstellen Funktionsobjekte ein Annotations-Dictionary bei Bedarf, wenn keine Annotationen auf diesem Objekt definiert sind. Sie können das __annotations__ Attribut mit del fn.__annotations__ löschen, aber wenn Sie dann auf fn.__annotations__ zugreifen, erstellt das Objekt ein neues leeres Dictionary, das es speichert und als seine Annotationen zurückgibt. Das Löschen der Annotationen einer Funktion, bevor sie ihr Annotations-Dictionary bedarfsgesteuert erstellt hat, löst einen AttributeError aus; das zweimalige Hintereinander-Löschen von Annotationen mit del fn.__annotations__ garantiert immer einen AttributeError.
Alles im obigen Absatz gilt auch für Klassen- und Modulobjekte in Python 3.10 und neuer.
In allen Versionen von Python 3 können Sie __annotations__ für ein Funktionsobjekt auf None setzen. Allerdings wird der anschließende Zugriff auf die Annotationen dieses Objekts über fn.__annotations__ ein leeres Dictionary bei Bedarf erstellen, wie im ersten Absatz dieses Abschnitts beschrieben. Dies gilt *nicht* für Module und Klassen in irgendeiner Python-Version; diese Objekte erlauben das Setzen von __annotations__ auf jeden Python-Wert und behalten den gesetzten Wert.
Wenn Python Ihre Annotationen für Sie stringifiziert (mit from __future__ import annotations) und Sie eine Zeichenkette als Annotation angeben, wird die Zeichenkette selbst zitiert. Effektiv ist die Annotation *zweimal* zitiert. Zum Beispiel
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
Dies gibt {'a': "'str'"} aus. Dies sollte eigentlich nicht als "Quirk" betrachtet werden; es wird hier nur erwähnt, weil es überraschend sein könnte.
Wenn Sie eine Klasse mit einer benutzerdefinierten Metaklasse verwenden und auf __annotations__ der Klasse zugreifen, können Sie unerwartetes Verhalten beobachten; siehe 749 für einige Beispiele. Sie können diese Quirks vermeiden, indem Sie annotationlib.get_annotations() auf Python 3.14+ oder inspect.get_annotations() auf Python 3.10+ verwenden. Auf früheren Python-Versionen können Sie diese Fehler vermeiden, indem Sie auf die Annotationen aus dem __dict__ der Klasse zugreifen (z. B. cls.__dict__.get('__annotations__', None)).
In einigen Python-Versionen können Instanzen von Klassen ein __annotations__ Attribut haben. Dies ist jedoch keine unterstützte Funktionalität. Wenn Sie die Annotationen einer Instanz benötigen, können Sie type() verwenden, um auf ihre Klasse zuzugreifen (z. B. annotationlib.get_annotations(type(myinstance)) auf Python 3.14+).