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 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, 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.