Boas práticas para anotações
****************************

autor:
   Larry Hastings


Resumo
^^^^^^

Este documento foi projetado para encapsular as melhores práticas para
trabalhar com anotações. Se você escrever um código Python que examina
"__annotations__" nos objetos Python, nós o encorajamos a seguir as
diretrizes descritas abaixo.

Este documento está dividido em quatro seções: melhores práticas para
acessar as anotações de um objeto no Python  na versão 3.10 e versões
mais recente, melhores práticas para acessar as anotações de um objeto
no Python na versão 3.9 e versões mais antiga, outras melhores
práticas para "__annotations__" para qualquer versão do Python e
peculiaridades do "__annotations__".

Note que este documento é específico sobre trabalhar com
"__annotations__", não para o uso *de* anotações. Se você está
procurando por informações sobre como usar "type hints" no seu código,
por favor veja o módulo "typing"


Acessando o dicionário de anotações de um objeto no Python 3.10 e nas versões mais recentes
===========================================================================================

O Python 3.10 adicionou uma nova função para a biblioteca padrão:
"inspect.get_annotations()". No Python 3.10 e nas versões mais
recentes, chamar esta função é a melhor prática para acessar o
dicionário de anotações de qualquer objeto com suporte a anotações.
Esta função pode até "destextualizar" anotações textualizadas para
você.

Se por alguma razão "inspect.get_annotations()" não for viável para o
seu caso de uso, você pode acessar o membro de dados "__annotations__"
manualmente. As melhores práticas para isto também mudaram no Python
3.10: a partir do Python 3.10, "o.__annotations__" é garantido
*sempre* funcionar em funções, classes e módulos Python. Se você tem
certeza que o objeto que você está examinando é um desses três
*exatos* objetos, pode simplesmente usar "o.__annotations__" para
chegar no dicionário de anotações do objeto.

Contudo, outros tipos de chamáveis -- por exemplo, chamáveis criados
por "functools.partial()" -- podem não ter um atributo
"__annotations__" definido. Ao acessar o "__annotations__" de um
objeto possivelmente desconhecido, as melhores práticas nas versões de
Python 3.10 e mais novas é chamar "getattr()" com três argumentos, por
exemplo "getattr(o, '__annotations__', None)".

Antes de Python 3.10, acessar "__annotations__" numa classe que não
define anotações mas que possui uma classe base com anotações retorna
o "__annotations__" da classe pai. A partir do Python 3.10, a anotação
da classe filha será um dicionário vazio.


Acessando o dicionário de anotações de um objeto no Python 3.9 e nas versões mais antigas
=========================================================================================

Em Python 3.9 e versões mais antigas, acessar o dicionário de
anotações de um objeto é muito mais complicado que em versões mais
novas. O problema é uma falha de design em versões mais antigas do
Python, especificamente a respeito de anotações de classe.

As melhores práticas para acessar os dicionários de anotações de
outros objetos, funções, outros chamáveis e módulos - são as mesmas
melhores práticas para 3.10, supondo que você não esteja chamando
"inspect.get_annotations()": você deve usar a função "getattr()" com
três argumentos para acessar os atributos do objeto "__annotations__".

Infelizmente, essas não são as melhores práticas para classes. O
problema é que, como "__annotations__" é opcional nas classes, e posto
que classes podem herdar atributos das suas classes base, acessar o
atributo "__annotations__" de uma classe pode inesperadamente retornar
o dicionário de anotações de uma *classe base.*  Por exemplo:

   class Base:
       a: int = 3
       b: str = 'abc'

   class Derived(Base):
       pass

   print(Derived.__annotations__)

Isso mostrará o dicionário de anotações de "Base", não de "Derived".

Seu código deve seguir uma abordagem diferente caso o objeto que você
esteja examinando seja uma classe ("isinstance(o, type)"). Nesse caso,
a melhor prática está ligada a um detalhe de implementação do Python
3.9 e anteriores: se a classe possui anotações definidas, elas são
armazenadas no dicionário "__dict__" da classe. Como a classe pode ou
não ter anotações definidas, a melhor prática é chamar o método "get"
no dicionário da classe.

Considerando tudo isso, aqui está um exemplo de código que acessa de
forma segura o atributo "__annotations__" em um objeto arbitrário em
Python 3.9 e anteriores:

   if isinstance(o, type):
       ann = o.__dict__.get('__annotations__', None)
   else:
       ann = getattr(o, '__annotations__', None)

Após executar este código, "ann" deve ser um dicionário ou "None".
Você é encorajado a fazer uma checagem dupla do tipo de "ann"
utilizando "isinstance()" antes de uma análise mais aprofundada.

Note que alguns tipos de objetos exóticos ou mal formatados podem não
possuir um atributo "__dict__", então por precaução você também pode
querer usar "getattr()" para acessar "__dict__".


Recuperando manualmente anotações transformadas em strings
==========================================================

Em situações em que as anotações podem ter sido transformadas em
strings, e caso queira avaliar essas strings para produzir os valores
de Python que elas representam, é melhor chamar
"inspect.get_annotations()" para fazer isto por você.

Se estiver usando Python 3.9 ou anterior, ou por algum motivo não
puder usar "inspect.get_annotations()", será necessário replicar sua
lógica. Considere examinar a implementação de
"inspect.get_annotations()" na versão de Python atual e seguir uma
abordagem similar.

Resumindo, caso deseje avaliar uma anotação transformada em string em
um objeto arbitrário "o":

* Se "o" for um módulo, use "o.__dict__" como "globals" ao chamar
  "eval()".

* Se "o" for uma classe, use "sys.modules[o.__module__].__dict__" como
  "globals", e "dict(vars(o))" como "locals", quando chamar "eval()".

* Se "o" for um chamável envolto em um invólucro usando
  "functools.update_wrapper()", "functools.wraps()" ou
  "functools.partial()", desenvolva-o iterativamente acessando
  "o.__wrapped__" ou "o.func" conforme apropriado, até encontrar a
  função raiz.

* Caso "o" seja chamável (mas não uma classe), use "o.__globals__"
  como globals quando chamar "eval()".

Contudo, nem todas strings usadas como anotações podem ser convertidas
em valores de Python utilizando "eval()". Valores de string poderiam
teoricamente conter qualquer string válida, e na prática existem casos
válidos para dicas de tipo que requerem anotar com valores de strings
que *não* podem ser executados. Por exemplo:

* Tipos de união **PEP 604** usando "|", antes do suporte a isso ser
  adicionado em Python 3.10.

* Definições que não são necessárias no ambiente de execução, apenas
  importadas quando "typing.TYPE_CHECKING" é verdadeiro.

Caso "eval()" tente executar tais valores, levantará uma exceção.
Então, quando projetar uma biblioteca API que trabalha com anotações,
é recomendado que tente executar os valores das strings apenas quando
for solicitado explicitamente.


Melhores práticas para "__annotations__" em qualquer versão Python
==================================================================

* Evite atribuir diretamente ao membro "__annotations__" dos objetos.
  Deixe que Python gerenciar a configuração de "__annotations__".

* Se você atribuir diretamente ao membro "__annotations__" do objeto,
  sempre o defina como um objeto "dict".

* Caso acesse diretamente o membro "__annotations__" de um objeto,
  certifique-se de que ele é um dicionário antes de tentar examinar
  seu conteúdo.

* Evite modificar o dicionário "__annotations__".

* Evite deletar o atributo "__annotations__" de um objeto.


Peculiaridades de "__annotations__"
===================================

Em todas as versões de Python 3, objetos de função criam
preguiçosamente um dicionário de anotações caso nenhuma anotação seja
definida nesse objeto. Você pode deletar o atributo "__annotations__"
utilizando "del fn.__annotations__", mas ao acessar
"fn.__annotations__" o objeto criará um novo dicionário vazio que será
armazenado e retornado como suas anotações. Apagar as anotações de uma
função antes que ela tenha criado preguiçosamente seu dicionário de
anotações irá produzir um "AttributeError"; ao utilizar "del
fn.__annotations__" duas vezes em sequência, é garantido que será
produzido um "AttributeError".

O citado no parágrafo acima também se aplica para classes e objetos de
módulo em Python 3.10 e posteriores.

Em todas as versões de Python 3, você pode definir "__annotations__"
como "None" em um objeto de função. Contudo, acessar em sequência as
anotações nesse objeto utilizando "fn.__annotations__" irá criar
preguiçosamente um dicionário vazio como descrito no primeiro
parágrafo dessa seção. Isso *não* é válido para módulos e classes, em
qualquer versão de Python; esses objetos permitem atribuir
"__annotations__" a qualquer valor de Python, e armazenam qualquer
valor atribuído.

Se Python transformar sua anotação em string (utilizando "from
__future__ import annotations"), e você especificar uma string como
anotação, essa string será posta entre aspas. Na prática a anotação
receberá aspas *duplas*. Por exemplo:

   from __future__ import annotations
   def foo(a: "str"): pass

   print(foo.__annotations__)

Isso exibe "{'a': "'str'"}". Não considere isso como uma
"peculiaridade"; foi mencionado aqui simplesmente porque pode ser
surpreendente.
