Boas práticas para anotações¶
- autor:
Larry Hastings
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 terá que ter um caminho de código separado se o objeto que você está examinando for uma classe (isinstance(o, type)
). Nesse caso, a melhor prática depende de um detalhe de implementação do Python 3.9 e anteriores: se uma classe tiver anotações definidas, elas serã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 dict 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.
Observe que alguns objetos de tipos exóticos ou malformados podem não ter atributo um __dict__
, portanto, para maior segurança, talvez você também queira 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, 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.Caso
o
seja chamável (mas não uma classe), useo.__globals__
como globals quando chamareval()
.
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__
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.