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änd o.__dict__ som globals när du anropar eval().

  • Om o är en klass, använd sys.modules[o.__module__].__dict__ som globals, och dict(vars(o)) som locals, när du anropar eval().

  • Om o är en inkapslad anropbar funktion som använder functools.update_wrapper(), functools.wraps(), eller functools.partial(), kan du iterativt packa upp den genom att använda antingen o.__wrapped__ eller o.func, tills du har hittat den uppackade rotfunktionen.

  • Om o är en callable (men inte en class), använd o.__globals__ som globals när du anropar eval().

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 ett dict-objekt.

  • Du bör undvika att komma åt __annotations__ direkt på något objekt. Använd istället annotationlib.get_annotations() (Python 3.14+) eller inspect.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+).