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étodogetno 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,
anndeve ser um dicionário ouNone. Você é encorajado a fazer uma checagem dupla do tipo deannutilizandoisinstance()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
ofor um módulo, useo.__dict__comoglobalsao chamareval().Se
ofor uma classe, usesys.modules[o.__module__].__dict__comoglobals, edict(vars(o))comolocals, quando chamareval().Se
ofor um chamável envolto em um invólucro usandofunctools.update_wrapper(),functools.wraps()oufunctools.partial(), desenvolva-o iterativamente acessandoo.__wrapped__ouo.funcconforme apropriado, até encontrar a função raiz.If
ois 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__comoNoneem 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.