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.

   * If "o" is a callable (but not a class), use "o.__globals__" as
     the globals when calling "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.
