Bästa praxis för annoteringar¶
- författare:
Larry Hastings
Åtkomst till Annotations Dict of An Object i Python 3.10 och nyare¶
Python 3.10 lägger till en ny funktion i standardbiblioteket: inspect.get_annotations()
. I Python-versionerna 3.10 till 3.13 är det bästa sättet att komma åt annotationsdikten för alla objekt som stöder annotationer att anropa den här funktionen. Denna funktion kan också ”avstränga” strängade annoteringar åt dig.
I Python 3.14 finns en ny modul annotationlib
med funktionalitet för att arbeta med annoteringar. Detta inkluderar en annotationlib.get_annotations()
-funktion, som ersätter inspect.get_annotations()
.
Om inspect.get_annotations()
av någon anledning inte är användbar för ditt användningsfall, kan du komma åt __annotations__
datamedlemmen manuellt. Bästa praxis för detta ändrades också i Python 3.10: från och med Python 3.10 garanteras o.__annotations__
att alltid fungera på Python-funktioner, klasser och moduler. Om du är säker på att objektet du undersöker är ett av dessa tre specifika objekt, kan du helt enkelt använda o.__annotations__
för att komma åt objektets annotationsdiktat.
Andra typer av callables – till exempel callables som skapats av functools.partial()
– har dock inte nödvändigtvis ett attribut __annotations__
definierat. När du kommer åt __annotations__
för ett eventuellt okänt objekt är bästa praxis i Python version 3.10 och senare att anropa getattr()
med tre argument, till exempel getattr(o, '__annotations__', None)
.
Före Python 3.10 skulle åtkomst till __annotations__
på en klass som inte definierar några annoteringar men som har en föräldraklass med annoteringar returnera förälderns __annotations__
. I Python 3.10 och nyare kommer barnklassens annotationer att vara en tom dict istället.
Åtkomst till Annotations Dict of An Object i Python 3.9 och äldre¶
I Python 3.9 och äldre är det mycket mer komplicerat att komma åt annotationsdikten för ett objekt än i nyare versioner. Problemet är ett designfel i dessa äldre versioner av Python, särskilt när det gäller klassannoteringar.
Bästa praxis för att komma åt annotationsdikten för andra objekt - funktioner, andra anropsbara objekt och moduler - är densamma som bästa praxis för 3.10, förutsatt att du inte anropar inspect.get_annotations()
: du bör använda treargument getattr()
för att komma åt objektets __annotations__
-attribut.
Tyvärr är detta inte bästa praxis för klasser. Problemet är att eftersom __annotations__
är valfritt för klasser, och eftersom klasser kan ärva attribut från sina basklasser, kan åtkomst till __annotations__
-attributet för en klass oavsiktligt returnera annotationsdikten för en basklass. Som ett exempel:
klass Bas:
a: int = 3
b: str = 'abc'
klass Avledda(Bas):
pass
print(Avledda.__annotationer__)
Detta kommer att skriva ut annoteringsdikten från Base
, inte Derived
.
Din kod måste ha en separat kodväg om objektet du undersöker är en klass (isinstance(o, type)
). I det fallet förlitar sig bästa praxis på en implementeringsdetalj i Python 3.9 och tidigare: om en klass har annotationer definierade lagras de i klassens __dict__
-ordbok. Eftersom klassen kanske eller kanske inte har annotationer definierade är bästa praxis att anropa metoden get()
på klassen dict.
För att sammanfatta det hela, här är några exempel på kod som på ett säkert sätt kommer åt attributet __annotations__
på ett godtyckligt objekt i Python 3.9 och tidigare:
if isinstance(o, typ):
ann = o.__dict__.get('__annotations__', None)
else:
ann = getattr(o, '__annotations__', None)
Efter att ha kört den här koden bör ann
vara antingen en dictionary eller None
. Du uppmanas att dubbelkolla typen av ann
med hjälp av isinstance()
innan vidare undersökning.
Observera att vissa exotiska eller missbildade typobjekt kanske inte har ett attribut __dict__
, så för extra säkerhet kan du också vilja använda getattr()
för att komma åt __dict__
.
Manuell avsträngning av strängade annoteringar¶
I situationer där vissa annoteringar kan vara ”strängade”, och du vill utvärdera dessa strängar för att producera de Python-värden de representerar, är det verkligen bäst att anropa inspect.get_annotations()
för att göra detta arbete åt dig.
Om du använder Python 3.9 eller äldre, eller om du av någon anledning inte kan använda inspect.get_annotations()
, måste du duplicera dess logik. Du uppmuntras att undersöka implementeringen av inspect.get_annotations()
i den aktuella Python-versionen och följa ett liknande tillvägagångssätt.
I ett nötskal, om du vill utvärdera en strängformad annotering på ett godtyckligt objekt o
:
Om
o
är en modul, användo.__dict__
somglobals
när du anropareval()
.Om
o
är en klass, användsys.modules[o.__module__].__dict__
somglobals
, ochdict(vars(o))
somlocals
, när du anropareval()
.Om
o
är en inkapslad anropbar funktion som använderfunctools.update_wrapper()
,functools.wraps()
, ellerfunctools.partial()
, kan du iterativt packa upp den genom att använda antingeno.__wrapped__
ellero.func
, tills du har hittat den uppackade rotfunktionen.Om
o
är en callable (men inte en class), användo.__globals__
som globals när du anropareval()
.
Det är dock inte alla strängvärden som används som annotationer som framgångsrikt kan omvandlas till Python-värden med eval()
. Strängvärden kan teoretiskt sett innehålla vilken giltig sträng som helst, och i praktiken finns det giltiga användningsfall för typtips som kräver annotering med strängvärden som specifikt inte kan utvärderas. Ett exempel:
PEP 604 unionstyper som använder
|
, innan stöd för detta lades till i Python 3.10.Definitioner som inte behövs vid körning, importeras endast när
typing.TYPE_CHECKING
är true.
Om eval()
försöker utvärdera sådana värden kommer det att misslyckas och ge upphov till ett undantag. Så när du utformar ett biblioteks-API som arbetar med annotationer rekommenderas att du bara försöker utvärdera strängvärden när det uttryckligen begärs av den som anropar.
Bästa praxis för __annotations__
i alla Python-versioner¶
Du bör undvika att tilldela objekt direkt till medlemmen
__annotations__
. Låt Python hantera inställningen av__annotations__
.Om du tilldelar direkt till medlemmen
__annotations__
i ett objekt, bör du alltid ställa in den till ettdict
-objekt.Du bör undvika att komma åt
__annotations__
direkt på något objekt. Använd iställetannotationlib.get_annotations()
(Python 3.14+) ellerinspect.get_annotations()
(Python 3.10+).Om du har direktåtkomst till medlemmen
__annotations__
i ett objekt bör du kontrollera att det är en ordbok innan du försöker undersöka innehållet.Du bör undvika att modifiera
__annotations__
dicts.Du bör undvika att ta bort attributet
__annotations__
för ett objekt.
__annotations__
Märkligheter¶
I alla versioner av Python 3 skapar funktionsobjekt latent en annotationsdikt om inga annotationer är definierade på objektet. Du kan ta bort attributet __annotations__
med del fn.__annotations__
, men om du sedan använder fn.__annotations__
kommer objektet att skapa en ny tom dict som den kommer att lagra och returnera som sina annotationer. Om du tar bort anteckningarna på en funktion innan den har skapat sin diktat med anteckningar kommer du att få ett AttributeError
; om du använder del fn.__annotations__
två gånger i rad kommer du garanterat alltid att få ett AttributeError
.
Allt i ovanstående stycke gäller även för klass- och modulobjekt i Python 3.10 och senare.
I alla versioner av Python 3 kan du ställa in __annotations__
på ett funktionsobjekt till None
. Men om du sedan kommer åt anteckningarna på det objektet med hjälp av fn.__annotations__
kommer en tom ordbok att skapas enligt första stycket i detta avsnitt. Detta är inte sant för moduler och klasser, i någon Python-version; dessa objekt tillåter inställning av __annotations__
till något Python-värde, och kommer att behålla det värde som är inställt.
Om Python strängar dina annotationer åt dig (med hjälp av from __future__ import annotations
), och du anger en sträng som en annotation, kommer strängen själv att citeras. I själva verket citeras annotationen två gånger. Till exempel:
from __future__ import annotations
def foo(a: "str"): pass
print(foo.__annotations__)
Detta skriver ut {'a': "'str'"}
. Detta bör egentligen inte betraktas som en ”finurlighet”; det nämns här helt enkelt för att det kan vara överraskande.
Om du använder en klass med en anpassad metaklass och kommer åt __annotations__
på klassen, kan du observera oväntat beteende; se 749 för några exempel. Du kan undvika dessa konstigheter genom att använda annotationlib.get_annotations()
på Python 3.14+ eller inspect.get_annotations()
på Python 3.10+. På tidigare versioner av Python kan du undvika dessa buggar genom att komma åt anteckningarna från klassens __dict__
(till exempel cls.__dict__.get('__annotations__', None)
).
I vissa versioner av Python kan instanser av klasser ha attributet __annotations__
. Detta är dock inte en funktionalitet som stöds. Om du behöver anteckningarna för en instans kan du använda type()
för att komma åt dess klass (till exempel annotationlib.get_annotations(type(myinstance))
på Python 3.14+).