Prácticas recomendadas para las anotaciones¶
- autor
Larry Hastings
Resumen
Este documento se designó para encapsular las prácticas recomendadas para trabajar con diccionarios de anotaciones. Si escribe código Python que examina __annotations__
en objetos Python, recomendamos seguir las pautas que se describen a continuación.
El documento se organizó en 4 secciones: prácticas recomendadas para acceder a las anotaciones de un objeto en las versiones de Python 3.10 y posteriores, prácticas recomendadas para acceder a las anotaciones de un objeto en las versiones de Python 3.9 y anteriores, otras prácticas recomendadas para __annotations__
que aplican a cualquier versión de Python y peculiaridades de __annotations__
.
Tome en cuenta que este documento trabaja específicamente con __annotations__
, no usarlo para anotaciones. Si está buscando información sobre cómo usar «anotaciones de tipado» en su código, consulte el módulo typing
.
Acceder al diccionario de anotaciones de un objeto en las versiones de Python 3.10 y posteriores¶
Python 3.10 agrega una nueva función a la biblioteca estándar:
inspect.get_annotations()
. En las versiones de Python 3.10 y posteriores, llamar esta función es la mejor práctica para acceder al diccionario de anotaciones de cualquier objeto que admita anotaciones. También esta función puede «desencadenar» anotaciones en cadena.Si por alguna razón
inspect.get_annotations()
no es viable para su caso de uso, puede acceder manualmente al miembro de datos__annotations__
. La práctica recomendada para esto también cambió en Python 3.10: a partir de Python 3.10, se garantiza queo.__annotations__
siempre opere en funciones, clases y módulos de Python. Si está seguro de que el objeto que está examinando es uno de los tres objetos específicos, puede usar simplementeo.__annotations__
para obtener el diccionario de anotaciones del objeto.Sin embargo, otros tipos de llamadas – por ejemplo, invocables creados por
functools.partial()
– pueden no tener definido un atributo__annotations__
. Al acceder a__annotations__
de un objeto posiblemente desconocido, la práctica recomendada de las versiones de Python 3.10 y posteriores es llamargetattr()
con tres argumentos, por ejemplogetattr(o, '__annotations__', None)
.Before Python 3.10, accessing
__annotations__
on a class that defines no annotations but that has a parent class with annotations would return the parent’s__annotations__
. In Python 3.10 and newer, the child class’s annotations will be an empty dict instead.
Acceder al diccionario de anotaciones de un objeto en las versiones de Python 3.9 y anteriores¶
En versiones de Python 3.9 y anteriores, acceder al diccionario de anotaciones de un objeto es mucho más complicado que en versiones más recientes. El problema es un defecto de diseño en estas versiones anteriores de Python, específicamente relacionado con las anotaciones de clase.
La práctica recomendada para acceder al diccionario de anotaciones de otros objetos – funciones, otros invocables y módulos – es la misma que la de la 3.10, asumiendo que no está llamando
inspect.get_annotations()
: debe usar tres argumentos degetattr()
para acceder al atributo__annotations__
del objeto.Desafortunadamente, esta no es la mejor práctica para las clases. El problema es que, dado que
__annotations__
es opcional en las clases, y debido a que las clases pueden heredar atributos desde sus clases base, acceder al atributo__annotations__
de una clase puede retornar por inadvertencia el diccionario de anotaciones de una clase base. Como ejemplo:class Base: a: int = 3 b: str = 'abc' class Derived(Base): pass print(Derived.__annotations__)Esto imprimirá el diccionario de anotaciones de
Base
, no deDerived
.Su código deberá tener una ruta de código separada si el objeto que está examinando es una clase (
isinstance(o, type)
). En este caso, la práctica recomendada se basa en un detalle de implementación de las versiones de Python 3.9 y anteriores: si una clase tiene anotaciones definidas, se almacenan en el diccionario__dict__
de la clase. Dado que la clase puede o no tener anotaciones definidas, la mejor práctica es llamar al métodoget
en el diccionario de la clase.Para ponerlo todo junto, aquí hay un código de muestra que accede de forma segura al atributo
__annotations__
en un objeto arbitrario en las versiones de Python 3.9 y anteriores:if isinstance(o, type): ann = o.__dict__.get('__annotations__', None) else: ann = getattr(o, '__annotations__', None)Después de ejecutar este código,
ann
debería ser un diccionario oNone
. Recomendamos que vuelva a verificar el tipo deann
usandoisinstance()
antes de un examen más detenido.Tome en cuenta que algunos objetos de tipo exótico o con formato incorrecto pueden no tener un atributo
__dict__
, así que para mayor seguridad, también puede usargetattr()
para acceder a__dict__
.
Desencadenamiento manual de anotaciones en cadena¶
En situaciones donde algunas anotaciones pueden estar «encadenadas», y desea evaluar esas cadenas de caracteres para producir los valores de Python que representan, lo mejor es llamar a
inspect.get_annotations()
para que haga este trabajo por usted.Si está usando la versión de Python 3.9 o anterior, o si por alguna razón no puede usar
inspect.get_annotations()
, necesitará duplicar su lógica. Recomendamos que examine la implementación deinspect.get_annotations()
en la versión actual de Python y siga un enfoque similar.En pocas palabras, si desea evaluar una anotación en cadena en un objeto arbitrario
o
:
Si
o
es un módulo, useo.__dict__
comoglobals
cuando llame aeval()
.Si
o
es una clase, usesys.modules[o.__module__].__dict__
comoglobals
ydict(vars(o))
comolocals
cuando llame aeval()
.Si
o
es un invocable envuelto usandofunctools.update_wrapper()
,functools.wraps()
ofunctools.partial()
, lo desenvuelve iterativamente accediendo ao.__wrapped__
oo.func
según corresponda, hasta que haya encontrado la función raíz sin envolver.Si
o
es un invocable (pero no una clase), useo.__globals__
comoglobals
cuando llame aeval()
.Sin embargo, no todos los valores de cadena de caracteres usados como anotaciones se pueden convertir correctamente en valores de Python mediante
eval()
. Los valores de cadena de caracteres pueden contener, teóricamente, cualquier cadena de caracteres válida, y en la práctica, hay varios casos de uso válidos para anotaciones de tipo que requieren anotaciones con valores de cadena de caracteres que no pueden evaluarse específicamente. Por ejemplo:
PEP 604 union types using
|
, before support for this was added to Python 3.10.Las definiciones que no son necesarias en tiempo de ejecución, sólo se importan cuando
typing.TYPE_CHECKING
es verdadero.Si
eval()
intenta evaluar tales valores, fallará y lanzará una excepción. Por lo tanto, al diseñar una API de biblioteca que funcione con anotaciones, se recomienda sólo intentar evaluar valores de cadena de caracteres cuando la llamada lo solicite explícitamente.
Prácticas recomendadas para __annotations__
en cualquier versión de Python¶
Debe evitar asignar directamente al miembro
__annotations__
de objetos. Deje que Python administre la configuración__annotations__
.Si asigna directamente al miembro
__annotations__
de un objeto, siempre debe establecerlo en un objetodict
.Si accede directamente al miembro
__annotations__
de un objeto, debe asegurarse de que sea un diccionario antes de intentar examinar su contenido.Debe evitar modificar los diccionarios
__annotations__
.Debe evitar eliminar el atributo
__annotations__
de un objeto.
Peculiaridades de __annotations__
¶
En todas las versiones de Python 3, los objetos de función crean de forma diferida un diccionario de anotaciones si no hay anotaciones definidas en ese objeto. Puede eliminar el atributo
__annotations__
usandodel fn.__annotations__
, pero si luego accede afn.__annotations__
, el objeto creará un diccionario nuevo vacío que almacenará y retornará como sus anotaciones. Eliminar las anotaciones en una función antes de que haya creado su diccionario de anotaciones de forma diferida arrojará unAttributeError
; el uso de dos veces seguidas dedel fn.__annotations__
garantiza que siempre arroje unAttributeError
.Todo en el párrafo anterior también se aplica a los objetos de clase y módulo en las versiones de Python 3.10 y posteriores.
En todas las versiones de Python 3, puede establecer
__annotations__
en un objeto de función enNone
. Sin embargo, al acceder después a las anotaciones en ese objeto usandofn.__annotations__
se creará un diccionario vacío de forma diferida según el primer párrafo de esta sección. Esto no es cierto para módulos y clases, en cualquier versión de Python; esos objetos permiten establecer__annotations__
en cualquier valor de Python, y conservarán cualquier valor que se establezca.Si Python encadena sus anotaciones por usted (usando
from __future__ import annotations
), y especifica una cadena de caracteres como una anotación, la cadena de caracteres en sí se citará. En efecto, la anotación se cita dos veces. Por ejemplo:from __future__ import annotations def foo(a: "str"): pass print(foo.__annotations__)Esto imprime
{'a': "'str'"}
. En realidad, esto no debería considerarse una «peculiaridad»; aquí se menciona simplemente porque podría sorprenderle.