
Acho que estamos nos acostumando lentamente ao fato de o Python ter anotações de tipo: elas foram trazidas de volta dois releases (3.5) nas anotações de funções e métodos ( PEP 484 ) e, no último release (3.6), às variáveis ( PEP 526 ).
Como os dois PEPs foram inspirados pelo MyPy , vou lhe dizer quais alegrias mundanas e dissonâncias cognitivas me aguardavam ao usar este analisador estático, bem como o sistema de digitação como um todo.
Disclamer: Não levanto a questão da necessidade ou nocividade da digitação estática em Python. Estou apenas falando das armadilhas que me deparei enquanto trabalhava em um contexto estaticamente digitado.
Genéricos (digitando.Generic)
É bom usar algo como List[int]
, Callable[[int, str], None]
nas anotações.
É muito bom quando o analisador destaca o seguinte código:
T = ty.TypeVar('T') class A(ty.Generic[T]): value: T A[int]().value = 'str'
No entanto, e se escrevermos uma biblioteca e o programador que a usar não usará um analisador estático?
Forçando o usuário a inicializar a classe com um valor e, em seguida, armazene seu tipo?
T = ty.TypeVar('T') class Gen(Generic[T]): value: T ref: Type[T] def __init__(self, value: T) -> None: self.value = value self.ref = type(value)
De alguma forma, não é fácil de usar.
Mas e se você quiser fazer isso?
b = Gen[A](B())
Em busca de uma resposta para essa pergunta, repassei um pouco a typing
e mergulhei no mundo das fábricas.

O fato é que, após inicializar a instância da classe Generic, ela possui o atributo __origin_class__
, que possui o atributo __args__
, que é uma tupla de tipo. No entanto, o acesso a partir de __init__
, bem como de __new__
, não é. Também não está na metaclasse __call__
. E o truque é que, no momento da inicialização da subclasse de Generic
ele se transforma em outra metaclasse _GenericAlias
, que define o tipo final, depois que o objeto é inicializado, incluindo todos os métodos de sua metaclasse, ou no momento em que __getithem__
. Portanto, não há como obter tipos genéricos ao construir um objeto.
Jogamos esse lixo, prometeu uma solução mais universal.Portanto, escrevi para mim um pequeno descritor que resolve esse problema:
def _init_obj_ref(obj: 'Gen[T]') -> None: """Set object ref attribute if not one to initialized arg.""" if not hasattr(obj, 'ref'): obj.ref = obj.__orig_class__.__args__[0]
Obviamente, em conseqüência, será necessário reescrever para uso mais universal, mas a essência é clara.
[UPD]: De manhã, decidi tentar fazer o mesmo que no próprio módulo de typing
, mas mais simples:
import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):
[UPD]: O desenvolvedor de typing
Ivan Levinsky disse que as duas opções podem quebrar imprevisivelmente.
De qualquer forma, você pode usar de qualquer maneira. Talvez __class_getitem__
seja ainda um pouco melhor, pelo menos __class_getitem__
é um método especial documentado (embora seu comportamento para genéricos não seja).
Funções e Aliases
Sim, os genéricos não são nada fáceis:
Por exemplo, se em algum lugar aceitarmos uma função como argumento, sua anotação passa automaticamente de covariante para contravariante:
class A: pass class B(A): pass def foo(arg: 'A') -> None:
E, em princípio, não tenho queixas sobre lógica, apenas isso deve ser resolvido através de aliases genéricos:
TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:
Em geral, a seção sobre variabilidade de tipos deve ser lida com cuidado e mais de uma vez.
Compatibilidade com versões anteriores
Isso não é tão bom: da versão 3.7 Generic
é uma subclasse do ABCMeta
, que é muito conveniente e boa. É ruim que isso quebre o código se estiver sendo executado no 3.6.
Herança Estrutural (Suputagem Estrutural)
No começo fiquei muito feliz: as interfaces foram entregues! O papel das interfaces é desempenhado pela classe Protocol
do módulo typing_extensions
, que, em combinação com o decorador @runtime
, permite verificar se a classe implementa a interface sem herança direta. O MyPy também é destacado em um nível mais profundo.
No entanto, não notei muitos benefícios práticos no tempo de execução em comparação à herança múltipla.
Parece que o decorador verifica apenas a presença de um método com o nome necessário, sem mesmo verificar o número de argumentos, sem mencionar a digitação:
import typing as ty import typing_extensions as te @te.runtime class IntStackP(te.Protocol): _list: ty.List[int] def push(self, val: int) -> None: ... class IntStack: def __init__(self) -> None: self._list: ty.List[int] = list() def push(self, val: int) -> None: if not isinstance(val, int): raise TypeError('wrong pushued val type') self._list.append(val) class StrStack: def __init__(self) -> None: self._list: ty.List[str] = list() def push(self, val: str, weather: ty.Any=None) -> None: if not isinstance(val, str): raise TypeError('wrong pushued val type') self._list.append(val) def push_func(stack: IntStackP, value: int): if not isinstance(stack, IntStackP): raise TypeError('is not IntStackP') stack.push(value) a = IntStack() b = StrStack() c: ty.List[int] = list() push_func(a, 1) push_func(b, 1)
Por outro lado, o MyPy, por sua vez, se comporta de maneira mais inteligente e destaca a incompatibilidade dos tipos:
push_func(a, 1) push_func(b, 1)
Sobrecarga do operador
Um tópico muito recente, porque ao sobrecarregar os operadores com toda a segurança do tipo, toda a diversão desaparece. Essa pergunta apareceu várias vezes no rastreador de erros do MyPy, mas ainda é difícil em alguns lugares, e você pode desativá-lo com segurança.
Eu explico a situação:
class A: def __add__(self, other) -> int: return 3 def __iadd__(self, other) -> 'A': if isinstance(other, int): return NotImplemented return A() var = A() var += 3
Se o método de atribuição composto retornar NotImplemented
, o Python procurará primeiro __radd__
, depois usará __add__
e voila.
O mesmo se aplica à sobrecarga de qualquer método de subclasse do formulário:
class A: def __add__(self, x : 'A') -> 'A': ... class B(A): @overload def __add__(self, x : 'A') -> 'A': ... @overload def __add__(self, x : 'B') -> 'B' : ...
Em alguns lugares, os avisos já foram movidos para a documentação, em alguns locais eles ainda trabalham no produto. Mas a conclusão geral dos colaboradores é deixar essas sobrecargas aceitáveis.