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 usaro.__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 é chamargetattr()
com três argumentos, por exemplogetattr(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çãogetattr()
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 deDerived
.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étodoget
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 ouNone
. Você é encorajado a fazer uma checagem dupla do tipo deann
utilizandoisinstance()
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 usargetattr()
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 deinspect.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, useo.__dict__
comoglobals
ao chamareval()
.Se
o
for uma classe, usesys.modules[o.__module__].__dict__
comoglobals
, edict(vars(o))
comolocals
, quando chamareval()
.Se
o
for um chamável envolto em um invólucro usandofunctools.update_wrapper()
,functools.wraps()
oufunctools.partial()
, desenvolva-o iterativamente acessandoo.__wrapped__
ouo.func
conforme apropriado, até encontrar a função raiz.If
o
is a callable (but not a class), useo.__globals__
as the globals when callingeval()
.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 objetodict
.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__
utilizandodel fn.__annotations__
, mas ao acessarfn.__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 umAttributeError
; ao utilizardel fn.__annotations__
duas vezes em sequência, é garantido que será produzido umAttributeError
.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__
comoNone
em um objeto de função. Contudo, acessar em sequência as anotações nesse objeto utilizandofn.__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.