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 que
"o.__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 simplemente
"o.__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 llamar "getattr()" con tres
argumentos, por ejemplo "getattr(o, '__annotations__', None)".

Antes de Python 3.10, acceder a "__annotations__" en una clase que no
define anotaciones pero que tiene una clase padre con anotaciones
devolvería las anotaciones de la clase padre. En Python 3.10 y
versiones posteriores, las anotaciones de la clase hija serán un
diccionario vacío en su lugar.


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 de "getattr()"
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 de
"Derived".

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étodo "get()" 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 o
"None". Recomendamos que vuelva a verificar el tipo de "ann" usando
"isinstance()" 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 usar "getattr()" 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 de
"inspect.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, use "o.__dict__" como "globals" cuando llame a
  "eval()".

* Si "o" es una clase, use "sys.modules[o.__module__].__dict__" como
  "globals" y "dict(vars(o))" como "locals" cuando llame a "eval()".

* Si "o" es un invocable envuelto usando "functools.update_wrapper()",
  "functools.wraps()" o "functools.partial()", lo desenvuelve
  iterativamente accediendo a "o.__wrapped__" o "o.func" según
  corresponda, hasta que haya encontrado la función raíz sin envolver.

* Si "o" es un invocable (pero no una clase), use "o.__globals__" como
  globales cuando llame a "eval()".

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** introduce tipos de unión usando "|", antes de que se
  agregara soporte para esto en 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 objeto "dict".

* 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__"
usando "del fn.__annotations__", pero si luego accede a
"fn.__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á un "AttributeError"; el uso de dos veces
seguidas de "del fn.__annotations__" garantiza que siempre arroje un
"AttributeError".

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 en "None". Sin embargo, al acceder después a
las anotaciones en ese objeto usando "fn.__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.
