Aprendi uma dolorosa lição: para programas pequenos, a tipagem dinâmica é ótima. Para programas grandes é necessária uma abordagem mais disciplinada. E ajuda se a linguagem der a você aquela disciplina, ao invés de dizer "Bem, faça o que quiser".[1]
um fã do Monty Python
Esse capítulo é uma continuação do [type_hints_in_def_ch], e fala mais sobre o sistema de tipagem gradual do Python. Os tópicos principais são:
-
Assinaturas de funções sobrepostas
-
typing.TypedDict: dando dicas de tipos paradictsusados como registros -
Coerção de tipo
-
Acesso a dicas de tipo durante a execução
-
Tipos genéricos
-
Declarando uma classe genérica
-
Variância: tipos invariantes, covariantes e contravariantes
-
Protocolos estáticos genéricos
-
Esse capítulo é inteiramente novo, escrito para essa segunda edição de Python Fluente. Vamos começar com sobreposições.
No Python, funções podem aceitar diferentes combinações de argumentos.
O decorador @typing.overload permite anotar tais combinações. Isso é particularmente importante quando o tipo devolvido pela função depende do tipo de dois ou mais parâmetros.
Considere a função embutida sum. Esse é o texto de help(sum).[2]:
>>> help(sum)
sum(iterable, /, start=0)
Devolve a soma de um valor 'start' (default: 0) mais a soma dos números de um iterável
Quando o iterável é vazio, devolve o valor inicial ('start').
Essa função é direcionada especificamente para uso com valores numéricos e pode rejeitar tipos não-numéricos.A função embutida sum é escrita em C, mas o typeshed tem dicas de tipos sobrepostas para ela, em builtins.pyi:
@overload
def sum(__iterable: Iterable[_T]) -> Union[_T, int]: ...
@overload
def sum(__iterable: Iterable[_T], start: _S) -> Union[_T, _S]: ...Primeiro, vamos olhar a sintaxe geral das sobreposições.
Esse acima é todo o código sobre sum que você encontrará no arquivo stub (.pyi).
A implementação estará em um arquivo diferente.
As reticências (…) não tem qualquer função além de cumprir a exigência sintática para um corpo de função, como no caso de pass.
Assim os arquivos .pyi são arquivos Python válidos.
Como mencionado na [arbitrary_arguments_sec], os dois sublinhados prefixando __iterable são a convenção da PEP 484 para argumentos apenas posicionais, que é verificada pelo Mypy.
Isso significa que você pode invocar sum(my_list), mas não sum(__iterable = my_list).
O verificador de tipo tenta fazer a correspondência entre os argumentos dados com cada assinatura sobreposta, em ordem.
A chamada sum(range(100), 1000) não casa com a primeira sobreposição, pois aquela assinatura tem apenas um parâmetro. Mas casa com a segunda.
Você pode também usar @overload em um modulo Python regular, colocando as assinaturas sobrepostas logo antes da assinatura real da função e de sua implementação.
O Exemplo 1 mostra como sum apareceria anotada e implementada em um módulo Python.
sum com assinaturaas sobrepostaslink:code/15-more-types/mysum.py[role=include]-
Precisamos deste segundo
TypeVarna segunda assinatura. -
Essa assinatura é para o caso simples:
sum(my_iterable). O tipo do resultado pode serT—o tipo dos elementos quemy_iterableproduz—ou pode serint, se o iterável for vazio, pois o valor default do parâmetrostarté0. -
Quando
starté dado, ele pode ser de qualquer tipoS, então o tipo do resultado éUnion[T, S]. É por isso que precisamos deS. SeTfosse reutilizado aqui, então o tipo destartteria que ser do mesmo tipo dos elementos deIterable[T]. -
A assinatura da implementação efetiva da função não tem dicas de tipo.
São muitas linhas para anotar uma função de uma única linha.
Sim, eu sei, provavelmente isso é excessivo.
Mas pelo menos a função do exemplo não é foo.
Se você quiser aprender sobre @overload lendo código, o typeshed tem centenas de exemplos.
Quando escrevo esse capítulo, o arquivo stub do typeshed para as funções embutidas do Python tem 186 sobreposições—mais que qualquer outro na biblioteca padrão.
|
Tip
|
Aproveite a tipagem gradual
Tentar produzir código 100% anotado pode levar a dicas de tipo que acrescentam muito ruído e pouco valor agregado. Refatoração para simplificar as dicas de tipo pode levar a APIs pesadas. Algumas vezes é melhor ser pragmático, e deixar parte do código sem dicas de tipo. |
As APIs convenientes e práticas que consideramos pythônicas são muitas vezes difíceis de anotar.
Na próxima seção veremos um exemplo:
são necessárias seis sobreposições para anotar adequadamente a flexível função embutida max.
É difícil acrescentar dicas de tipo a funções que usam os poderosos recursos dinâmicos do Python.
Quando estudava o typeshed, enconterei o relatório de bug #4051 (EN):
Mypy não avisou que é ilegal passar None como um dos argumentos para a função embutida max(), ou passar um iterável que em algum momento produz None.
Nos dois casos, você recebe uma exceção como a seguinte durante a execução:
TypeError: '>' not supported between instances of 'int' and 'NoneType' [NT: TypeError: '>' não é suportado entre instâncias de 'int' e 'NoneType']
A documentação de max começa com a seguinte sentença:
Devolve o maior item em um iterável ou o maior de dois ou mais argumentos.
Para mim, essa é uma descrição bastante intuitiva.
Mas se eu for anotar uma função descrita nesses termos, tenho que perguntar: qual dos dois? Um iterável ou dois ou mais argumentos?
A realidade é mais complicada, porque max também pode receber dois argumentos opcionais:
key e default.
Escrevi max em Python para tornar mais fácil ver a relação entre o funcionamento da função e as anotações sobrepostas (a função embutida original é escrita em C); veja o Exemplo 2.
max em Python# imports and definitions omitted, see next listing
MISSING = object()
EMPTY_MSG = 'max() arg is an empty sequence'
# overloaded type hints omitted, see next listing
link:code/15-more-types/protocol/mymax/mymax.py[role=include]O foco desse exemplo não é a lógica de max, então não vou perder tempo com a implementação, exceto para explicar MISSING.
A constante MISSING é uma instância única de object, usada como sentinela.
É o valor default para o argumento nomeado default=, de modo que max pode aceitar default=None e ainda assim distinguir entre duas situações.
Quando first é um iterável vazio…
-
O usuário não forneceu um argumento para
default=, então ele éMISSING, emaxgera umValueError. -
O usuário forneceu um valor para
default=, incluindoNone, e entãomaxdevolve o valor dedefault.
Para consertar o issue #4051, escrevi o código no Exemplo 3.[3]
link:code/15-more-types/protocol/mymax/mymax.py[role=include]Minha implementação de max em Python tem mais ou menos o mesmo tamanho daquelas importações e declarações de tipo.
Graças ao duck typing, meu código não tem nenhuma verificação usando isinstance, e fornece a mesma verificação de erro daquelas dicas de tipo—mas apenas durante a execução, claro.
Um benefício fundamental de @overload é declarar o tipo devolvido da forma mais precisa possível, de acordo com os tipos dos argumentos recebidos.
Veremos esse benefício a seguir, estudando as sobreposições de max, em grupos de duas ou três por vez.
@overload
def max(__arg1: LT, __arg2: LT, *_args: LT, key: None = ...) -> LT:
...
# ... lines omitted ...
@overload
def max(__iterable: Iterable[LT], *, key: None = ...) -> LT:
...Nesses casos, as entradas são ou argumentos separados do tipo LT que implementam SupportsLessThan, ou um Iterable de itens desse tipo.
O tipo devolvido por max é do mesmo tipo dos argumentos ou itens reais, como vimos na [bounded_typevar_sec].
Amostras de chamadas que casam com essas sobreposições:
max(1, 2, -3) # returns 2
max(['Go', 'Python', 'Rust']) # returns 'Rust'@overload
def max(__arg1: T, __arg2: T, *_args: T, key: Callable[[T], LT]) -> T:
...
# ... lines omitted ...
@overload
def max(__iterable: Iterable[T], *, key: Callable[[T], LT]) -> T:
...As entradas podem ser item separados de qualquer tipo T ou um único
Iterable[T], e key= deve ser um invocável que recebe um argumento do mesmo tipo T, e devolve um valor que implementa SupportsLessThan.
O tipo devolvido por max é o mesmo dos argumentos reais.
Amostras de chamadas que casam com essas sobreposições:
max(1, 2, -3, key=abs) # returns -3
max(['Go', 'Python', 'Rust'], key=len) # returns 'Python'@overload
def max(__iterable: Iterable[LT], *, key: None = ...,
default: DT) -> Union[LT, DT]:
...A entrada é um iterável de itens do tipo LT que implemente SupportsLessThan.
O argumento default= é o valor devolvido quando Iterable é vazio.
Assim, o tipo devolvido por max deve ser uma Union do tipo LT e do tipo do argumento default.
Amostras de chamadas que casam com essas sobreposições:
max([1, 2, -3], default=0) # returns 2
max([], default=None) # returns None@overload
def max(__iterable: Iterable[T], *, key: Callable[[T], LT],
default: DT) -> Union[T, DT]:
...As entradas são:
-
Um
Iterablede itens de qualquer tipoT -
Invocável que recebe um argumento do tipo
Te devolve um valor do tipoLT, que implementaSupportsLessThan -
Um valor default de qualquer tipo
DT
O tipo devolvido por max deve ser uma Union do tipo T e do tipo do argumento default:
max([1, 2, -3], key=abs, default=None) # returns -3
max([], key=abs, default=None) # returns NoneDicas de tipo permitem ao Mypy marcar uma chamada como max([None, None]) com essa mensagem de erro:
mymax_demo.py:109: error: Value of type variable "_LT" of "max" cannot be "None"
Por outro lado, ter de escrever tantas linhas para suportar o verificador de tipo pode desencorajar a criação de funções convenientes e flexíveis como max.
Se eu precisasse reinventar também a função min, poderia refatorar e reutilizar a maior parte da implementação de max.
Mas teria que copiar e colar todas as declarações de sobreposição—apesar delas serem idênticas para min, exceto pelo nome da função.
Meu amigo João S. O. Bueno—um dos desenvolvedores Python mais inteligentes que conheço—escreveu o seguinte tweet:
Apesar de ser difícil expressar a assinatura de
max—ela se encaixa muito facilmente em nossa estrutura mental. Considero a expressividade das marcas de anotação muito limitadas, se comparadas à do Python.
Vamos agora examinar o elemento de tipagem TypedDict.
Ele não é tão útil quanto imaginei inicialmente, mas tem seus usos.
Experimentar com TypedDict demonstra as limitações da tipagem estática para lidar com estruturas dinâmicas, tais como dados em formato JSON.
|
Warning
|
É tentador usar |
Algumas vezes os dicionários do Python são usados como registros, as chaves interpretadas como nomes de campos e os valores como valores dos campos de diferentes tipos. Considere, por exemplo, um registro descrevendo um livro, em JSON ou Python:
{"isbn": "0134757599",
"title": "Refactoring, 2e",
"authors": ["Martin Fowler", "Kent Beck"],
"pagecount": 478}Antes do Python 3.8, não havia uma boa maneira de anotar um registro como esse, pois os tipos de mapeamento que vimos na [mapping_type_sec] limitam os valores a um mesmo tipo.
Aqui estão duas tentativas ruins de anotar um registro como o objeto JSON acima:
Dict[str, Any]-
Os valores podem ser de qualquer tipo.
Dict[str, Union[str, int, List[str]]]-
Difícil de ler, e não preserva a relação entre os nomes dos campos e seus respectivos tipos:
titledeve ser umastr, ele não pode ser umintou umaList[str].
A PEP 589—TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys (TypedDict: Dicas de Tipo para Dicionários com um Conjunto Fixo de Chaves) enfrenta esse problema. O Exemplo 4 mostra um TypedDict simples.
BookDictlink:code/15-more-types/typeddict/books.py[role=include]À primeira vista, typing.TypedDict pode parecer uma fábrica de classes de dados, similar a typing.NamedTuple—tratada no [data_class_ch].
A similaridade sintática é enganosa. TypedDict é muito diferente.
Ele existe apenas para o benefício de verificadores de tipo, e não tem qualquer efeito durante a execução.
TypedDict fornece duas coisas:
-
Uma sintaxe similar à de classe para anotar uma
dictcom dicas de tipo para os valores de cada "campo". -
Um construtor que informa ao verificador de tipo para esperar um
dictcom chaves e valores como especificados.
Durante a execução, um construtor de TypedDict como BookDict é um placebo:
ele tem o mesmo efeito de uma chamada ao construtor de dict com os mesmos argumentos.
O fato de BookDict criar um dict simples também significa que:
-
Os "campos" na definiçao da pseudoclasse não criam atributos de instância.
-
Não é possível escrever inicializadores com valores default para os "campos".
-
Definições de métodos não são permitidas.
Vamos explorar o comportamento de um BookDict durante a execução (no Exemplo 5).
BookDict, mas não exatamente como planejado>>> from books import BookDict
>>> pp = BookDict(title='Programming Pearls', # (1)
... authors='Jon Bentley', # (2)
... isbn='0201657880',
... pagecount=256)
>>> pp # (3)
{'title': 'Programming Pearls', 'authors': 'Jon Bentley', 'isbn': '0201657880',
'pagecount': 256}
>>> type(pp)
<class 'dict'>
>>> pp.title # (4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'title'
>>> pp['title']
'Programming Pearls'
>>> BookDict.__annotations__ # (5)
{'isbn': <class 'str'>, 'title': <class 'str'>, 'authors': typing.List[str],
'pagecount': <class 'int'>}-
É possível invocar
BookDictcomo um construtor dedict, com argumentos nomeados, ou passando um argumentodict—incluindo um literaldict. -
Oops…Esqueci que
authorsdeve ser uma lista. Mas tipagem gradual significa que não há checagem de tipo durante a execução. -
O resultado da chamada a
BookDicté umdictsimples… -
…assim não é possível ler os campos usando a notação
objeto.campo. -
As dicas de tipo estão em
BookDict.__annotations__, e não empp.
Sem um verificador de tipo, TypedDict é tão útil quanto comentários em um programa:
pode ajudar a documentar o código, mas só isso.
As fábricas de classes do [data_class_ch], por outro lado,
são úteis mesmo se você não usar um verificador de tipo,
porque durante a execução elas geram uma classe personalizada que pode ser instanciada.
Elas também fornecem vários métodos ou funções úteis,
listadas na [builders_compared_tbl] do [data_class_ch].
O Exemplo 6 cria um BookDict válido e tenta executar algumas operações com ele.
A seguir, o Exemplo 7 mostra como TypedDict permite que o Mypy encontre erros.
BookDictlink:code/15-more-types/typeddict/demo_books.py[role=include]-
Lembre-se de adicionar o tipo devolvido, assim o Mypy não ignora a função.
-
Este é um
BookDictválido: todas as chaves estão presentes, com valores do tipo correto. -
O Mypy vai inferir o tipo de
authorsa partir da anotação na chave'authors'emBookDict. -
typing.TYPE_CHECKINGsó éTruequando os tipos no programa estão sendo verificados. Durante a execução ele é sempre falso. -
O
ifanterior evita quereveal_type(authors)seja chamado durante a execução.reveal_typenão é uma função do Python disponível durante a execução, mas sim um instrumento de depuração fornecido pelo Mypy. Por isso não há umimportpara ela. Veja sua saída no Exemplo 7. -
As últimas três linhas da função
demosão ilegais. Elas vão causar mensagens de erro no Exemplo 7.
…/typeddict/ $ mypy demo_books.py
demo_books.py:13: note: Revealed type is 'built-ins.list[built-ins.str]' (1)
demo_books.py:14: error: Incompatible types in assignment
(expression has type "str", variable has type "List[str]") (2)
demo_books.py:15: error: TypedDict "BookDict" has no key 'weight' (3)
demo_books.py:16: error: Key 'title' of TypedDict "BookDict" cannot be deleted (4)
Found 3 errors in 1 file (checked 1 source file)-
Essa observação é o resultado de
reveal_type(authors). -
O tipo da variável
authorsfoi inferido a partir do tipo da expressão que a inicializou,book['authors']. Você não pode atribuir umastrpara uma variável do tipoList[str]. Verificadores de tipo em geral não permitem que o tipo de uma variável mude.[4] -
Não é permitido atribuir a uma chave que não é parte da definição de
BookDict. -
Não se pode apagar uma chave que é parte da definição de
BookDict.
Vejamos agora BookDict sendo usado em assinaturas de função, para checar o tipo em chamadas de função.
Imagine que você precisa gerar XML a partir de registros de livros como esse:
<BOOK>
<ISBN>0134757599</ISBN>
<TITLE>Refactoring, 2e</TITLE>
<AUTHOR>Martin Fowler</AUTHOR>
<AUTHOR>Kent Beck</AUTHOR>
<PAGECOUNT>478</PAGECOUNT>
</BOOK>Se você estivesse escrevendo o código em MicroPython, para ser integrado a um pequeno microcontrolador, poderia escrever uma função parecida com a que aparece no Exemplo 8.[5]
to_xmllink:code/15-more-types/typeddict/books.py[role=include]-
O principal objetivo do exemplo: usar
BookDictem uma assinatura de função. -
Se a coleção começa vazia, o Mypy não tem inferir o tipo dos elementos. Por isso a anotação de tipo é necessária aqui.[6]
-
O Mypy entende testes com
isinstance, e tratavaluecomo umalistneste bloco. -
Quando usei
key == 'authors'como condição doifque guarda esse bloco, o Mypy encontrou um erro nessa linha:"object" has no attribute "__iter__"("object" não tem um atributo "__iter__" ), porque inferiu o tipo devaluedevolvido porbook.items()comoobject, que não suporta o método__iter__exigido pela expressão geradora. O teste comisinstancefunciona porque garante quevalueé umalistnesse bloco.
O Exemplo 9 mostra uma função que interpreta uma str JSON e devolve um BookDict.
from_jsonlink:code/15-more-types/typeddict/books_any.py[role=include]-
O tipo devolvido por
json.loads()éAny.[7] -
Posso devolver
whatever—de tipoAny—porqueAnyé consistente-com todos os tipos, incluindo o tipo declarado do valor devolvido,BookDict.
O segundo ponto do Exemplo 9 é muito importante de ter em mente:
O Mypy não vai apontar qualquer problema nesse código, mas durante a execução o valor em whatever pode não se adequar à estrutura de BookDict—na verdade, pode nem mesmo ser um dict!
Se você rodar o Mypy com --disallow-any-expr, ele vai reclamar sobre as duas linhas no corpo de from_json:
…/typeddict/ $ mypy books_any.py --disallow-any-expr
books_any.py:30: error: Expression has type "Any"
books_any.py:31: error: Expression has type "Any"
Found 2 errors in 1 file (checked 1 source file)As linhas 30 e 31 mencionadas no trecho acima são o corpo da função from_json.
Podemos silenciar o erro de tipo acrescentando uma dica de tipo à inicialização da variável whatever, como no Exemplo 10.
from_json com uma anotação de variávellink:code/15-more-types/typeddict/books.py[role=include]-
--disallow-any-exprnão gera erros quando uma expressão de tipoAnyé imediatamente atribuída a uma variável com uma dica de tipo. -
Agora
whateveré do tipoBookDict, o tipo declarado do valor devolvido.
|
Warning
|
Não se deixe enganar por uma falsa sensação de tipagem segura com o Exemplo 10!
Olhando o código estático, o verificador de tipo não tem como prever se |
A verificação de tipo estática é incapaz de prevenir erros cm código inerentemente dinâmico, como json.loads(), que cria objetos Python de tipos diferentes durante a execução. O Exemplo 11, o Exemplo 12 e o Exemplo 13 demonstram isso.
from_json devolve um BookDict inválido, e to_xml o aceitalink:code/15-more-types/typeddict/demo_not_book.py[role=include]-
Essa linha não produz um
BookDictválido—veja o conteúdo deNOT_BOOK_JSON. -
Vamos deixar o Mypy revelar alguns tipos.
-
Isso não deve causar problemas:
printconsegue lidar comobjecte com qualquer outro tipo. -
BookDictnão tem uma chave'flavor', mas o fonte JSON tem…o que vai acontecer?? -
Lembre-se da assinatura:
def to_xml(book: BookDict) → str:. -
Como será a saída XML?
Agora verificamos demo_not_book.py com o Mypy (no Exemplo 12).
…/typeddict/ $ mypy demo_not_book.py
demo_not_book.py:12: note: Revealed type is
'TypedDict('books.BookDict', {'isbn': built-ins.str,
'title': built-ins.str,
'authors': built-ins.list[built-ins.str],
'pagecount': built-ins.int})' (1)
demo_not_book.py:13: note: Revealed type is 'built-ins.list[built-ins.str]' (2)
demo_not_book.py:16: error: TypedDict "BookDict" has no key 'flavor' (3)
Found 1 error in 1 file (checked 1 source file)-
O tipo revelado é o tipo nominal, não o conteúdo de
not_bookdurante a execução. -
De novo, este é o tipo nominal de
not_book['authors'], como definido emBookDict. Não o tipo durante a execução. -
Esse erro é para a linha
print(not_book['flavor']): essa chave não existe no tipo nominal.
Agora vamos executar demo_not_book.py, mostrando o resultado no Exemplo 13.
demo_not_book.py…/typeddict/ $ python3 demo_not_book.py
{'title': 'Andromeda Strain', 'flavor': 'pistachio', 'authors': True} (1)
pistachio (2)
<BOOK> (3)
<TITLE>Andromeda Strain</TITLE>
<FLAVOR>pistachio</FLAVOR>
<AUTHORS>True</AUTHORS>
</BOOK>-
Isso não é um
BookDictde verdade. -
O valor de
not_book['flavor']. -
to_xmlrecebe um argumentoBookDict, mas não há qualquer verificação durante a execução: entra lixo, sai lixo.
O Exemplo 13 mostra que demo_not_book.py devolve bobagens, mas não há qualquer erro durante a execução.
Usar um TypedDict ao tratar dados em formato JSON não resultou em uma tipagem segura.
Olhando o código de to_xml no Exemplo 8 através das lentes do duck typing, o argumento book deve fornecer um método .items() que devolve um iterável de tuplas na forma (chave, valor), onde:
-
chavedeve ter um método.upper() -
valorpode ser qualquer coisa
A conclusão desta demonstração: quando estamos lidando com dados de estrutura dinâmica, tal como JSON ou XML, TypedDict não é, de forma alguma, um substituto para a validaçào de dados durante a execução. Para isso, use o pydantic (EN).
TypedDict tem mais recursos, incluindo suporte a chaves opcionais, uma forma limitada de herança e uma sintaxe de declaração alternativa. Para saber mais sobre ele, revise a
PEP 589—TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys (TypedDict: Dicas de Tipo para Dicionários com um Conjunto Fixo de Chaves) (EN).
Vamos agora voltar nossas atenções para uma função que é melhor evitar, mas que algumas vezes é inevitável: typing.cast.
Nenhum sistema de tipos é perfeito, nem tampouco os verificadores estáticos de tipo, as dicas de tipo no projeto typeshed ou as dicas de tipo em pacotes de terceiros que as oferecem.
A função especial typing.cast() fornece uma forma de lidar com defeitos ou incorreções nas dicas de tipo em código que não podemos consertar.
A documentação do Mypy 0.930 (EN) explica:
Coerções são usadas para silenciar avisos espúrios do verificador de tipos, e dão uma ajuda ao verificador quando ele não consegue entender direito o que está acontecendo.
Durante a execução, typing.cast não faz absolutamente nada. Essa é sua
implementação:
def cast(typ, val):
"""Cast a value to a type.
This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return valA PEP 484 exige que os verificadores de tipo "acreditem cegamente" em cast.
A seção "Casts" (Coerções) da PEP 484 mostra um exemplo onde o verificador precisa da orientação de cast:
link:code/15-more-types/cast/find.py[role=include]A chamada next() na expressão geradora vai devolver ou o índice de um item str ou gerar StopIteration. Assim, find_first_str vai sempre devolver uma str se não for gerada uma exceção, e str é o tipo declarado do valor devolvido.
Mas se a última linha for apenas return a[index], o Mypy inferiria o tipo devolvido como object, porque o argumento a é declarado como list[object].
Então cast() é necessário para guiar o Mypy.[8]
Aqui está outro exemplo com cast, desta vez para corrigir uma dica de tipo desatualizada na biblioteca padrão do Python.
No [tcp_mojifinder_main], criei um objeto asyncio , Server, e queria obter o endereço que o servidor estava ouvindo.
Escrevi essa linha de código:
addr = server.sockets[0].getsockname()Mas o Mypy informou o seguinte erro:
Value of type "Optional[List[socket]]" is not indexable
A dica de tipo para Server.sockets no typeshed, em maio de 2021, é válida para o Python 3.6, onde o atributo sockets podia ser None.
Mas no Python 3.7, sockets se tornou uma propriedade, com um getter que sempre devolve uma list—que pode ser vazia, se o servidor não tiver um socket.
E desde o Python 3.8, esse getter devolve uma tuple (usada como uma sequência imutável).
Já que não posso consertar o typeshed nesse instante,[9] acrescentei um cast, assim:
link:code/15-more-types/cast/tcp_echo.py[role=include]
# ... muitas linhas omitidas ...
link:code/15-more-types/cast/tcp_echo.py[role=include]Usar cast nesse caso exigiu algumas horas para entender o problema e ler o código-fonte de asyncio, para encontrar o tipo correto para sockets:
a classe TransportSocket do módulo não-documentado asyncio.trsock.
Também precisei adicionar duas instruções import e mais uma linha de código para melhorar a legibilidade.[10] Mas agora o código está mais seguro.
O leitor atento pode ser notado que sockets[0] poderia gerar um IndexError
se sockets estiver vazio.
Entretanto, até onde entendo o asyncio, isso não pode acontecer no [tcp_mojifinder_main], pois no momento em que leio o atributo sockets, o server já está pronto para aceitar conexões , portanto o atributo não estará vazio. E, de qualquer forma, IndexError ocorre durante a execução. O Mypy não consegue localizar esse problema nem mesmo em um caso trivial como print([][0]).
|
Warning
|
Não fique muito confortável usando |
Apesar de suas desvantagens, há usos válidos para cast.
Eis algo que Guido van Rossum escreveu sobre isso:
O que está errado com uma chamada a
cast()ou um comentário# type: ignoreocasionais?[11]
É insensato banir inteiramente o uso de cast, principalmente porque as alternativas para contornar esses problemas são piores:
-
# type: ignoreé menos informativo.[12] -
Usar
Anyé contagioso: já queAnyé consistente-com todos os tipos, seu abuso pode produzir efeitos em cascata através da inferência de tipo, minando a capacidade do verificador de tipo para detectar erros em outras partes do código.
Claro, nem todos os contratempos de tipagem podem ser resolvidos com cast.
Algumas vezes precisamos de # type: ignore, do Any ocasional, ou mesmo deixar uma função sem dicas de tipo.
A seguir, vamos falar sobre o uso de anotações durante a execução.
Durante a importação, o Python lê as dicas de tipo em funções, classes e módulos, e as armazena em atributos chamados __annotations__.
Considere, por exemplo, a função clip function no Exemplo 14.[13]
clipdef clip(text: str, max_len: int = 80) -> str:As dicas de tipo são armazenadas em um dict no atributo __annotations__ da função:
>>> from clip_annot import clip
>>> clip.__annotations__
{'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}A chave 'return' está mapeada para a dica do tipo devolvido após o símbolo → no Exemplo 14.
Observe que as anotações são avaliadas pelo interpretador no momento da importação, ao mesmo tempo em que os valores default dos parâmetros são avaliados.
Por isso os valores nas anotações são as classes Python str e int,
e não as strings 'str' and 'int'.
A avaliação das anotações no momento da importação é o padrão desde o Python 3.10,
mas isso pode mudar se a PEP 563 ou a PEP 649 se tornarem o comportamento padrão.
O aumento do uso de dicas de tipo gerou dois problemas:
-
Importar módulos usa mais CPU e memória quando são usadas muitas dicas de tipo.
-
Referências a tipos ainda não definidos exigem o uso de strings em vez do tipos reais.
As duas questões são relevantes.
A primeira pelo que acabamos de ver: anotações são avaliadas pelo interpretador durante a importação e armazenadas no atributo __annotations__.
Vamos nos concentrar agora no segundo problema.
Armazenar anotações como string é necessário algumas vezes, por causa do problema da "referência adiantada" (forward reference): quando uma dica de tipo precisa se referir a uma classe definida mais adiante no mesmo módulo. Entretanto uma manifestação comum desse problema no código-fonte não se parece de forma alguma com uma referência adiantada: quando um método devolve um novo objeto da mesma classe. Já que o objeto classe não está definido até o Python terminar a avaliação do corpo da classe, as dicas de tipo precisam usar o nome da classe como string. Eis um exemplo:
class Rectangle:
# ... lines omitted ...
def stretch(self, factor: float) -> 'Rectangle':
return Rectangle(width=self.width * factor)Escrever dicas de tipo com referências adiantadas como strings é a prática padrão e exigida no Python 3.10. Os verificadores de tipo estáticos foram projetados desde o início para lidar com esse problema.
Mas durante a execução, se você escrever código para ler a anotação return de stretch, vai receber a string 'Rectangle' em vez de uma referência ao tipo real, a classe Rectangle.
E aí seu código precisa descobrir o que aquela string significa.
O módulo typing inclui três funções e uma classe categorizadas
Introspection helpers (Auxiliares de introspecção),
a mais importantes delas sendo typing.get_type_hints.
Parte de sua documentação afirma:
get_type_hints(obj, globals=None, locals=None, include_extras=False)-
[…] Isso é muitas vezes igual a
obj.__annotations__. Além disso, referências adiantadas codificadas como strings literais são tratadas por sua avaliação nos espaços de nomesglobalselocals. […]
|
Warning
|
Desde o Python 3.10, a nova função |
A PEP 563—Postponed Evaluation of Annotations (_Avaliação Adiada de Anotações) (EN) foi aprovada para tornar desnecessário escrever anotações como strings, e para reduzir o custo das dicas de tipo durante a execução. A ideia principal está descrita nessas duas sentenças do "Abstract" (EN):
Esta PEP propõe modificar as anotações de funções e de variáveis, de forma que elas não mais sejam avaliadas no momento da definição da função. Em vez disso, elas são preservadas em __annotations__ na forma de strings..
A partir do Python 3.7, é assim que anotações são tratadas em qualquer módulo que comece com a seguinte instrução import:
from __future__ import annotationsPara demonstrar seu efeito, coloquei a mesma função clip do Exemplo 14 em um módulo clip_annot_post.py com aquela linha de importação __future__ no início.
No console, esse é o resultado de importar aquele módulo e ler as anotações de clip:
>>> from clip_annot_post import clip
>>> clip.__annotations__
{'text': 'str', 'max_len': 'int', 'return': 'str'}Como se vê, todas as dicas de tipo são agora strings simples, apesar de não terem sido escritas como strings na definição de clip (no Exemplo 14).
A função typing.get_type_hints consegue resolver muitas dicas de tipo, incluindo essas de clip:
>>> from clip_annot_post import clip
>>> from typing import get_type_hints
>>> get_type_hints(clip)
{'text': <class 'str'>, 'max_len': <class 'int'>, 'return': <class 'str'>}A chamada a get_type_hints nos dá os tipos resis—mesmo em alguns casos onde a dica de tipo original foi escrita como uma string.
Essa é a maneira recomendada de ler dicas de tipo durante a execução.
O comportamento prescrito na PEP 563 estava previsto para se tornar o default no Python
3.10, tornando a importação com __future__ desnecessária.
Entretanto, os mantenedores da FastAPI e do pydantic soaram o alarme, essa mudança quebraria seu código, que se baseia em dicas de tipo durante a execução e não podem usar get_type_hints de forma confiável.
Na discussão que se seguiu na lista de email python-dev, Łukasz Langa—autor da PEP 563—descreveu algumas limitações daquela função:
[…] a verdade é que
typing.get_type_hints()tem limites que tornam seu uso geral custoso durante a execução e, mais importante, insuficiente para resolver todos os tipos. O exemplo mais comum se refere a contextos não-globais nos quais tipos são gerados (isto é, classes aninhadas, classes dentro de funções, etc.). Mas um dos principais exemplos de referências adiantadas, classes com métodos aceitando ou devolvendo objetos de seu próprio tipo, também não é tratado de forma apropriada portyping.get_type_hints()se um gerador de classes for usado. Há alguns truques que podemos usar para ligar os pontos mas, de uma forma geral, isso não é bom.[14]
O Steering Council do Python decidiu adiar a elevação da PEP 563 a comportamento padrão até o Python 3.11 ou posterior, dando mais tempo aos desenvolvedores para criar uma solução para os problemas que a PEP 563 tentou resolver, sem quebrar o uso dissseminado das dicas de tipo durante a execução. A PEP 649—Deferred Evaluation Of Annotations Using Descriptors (Avaliação Adiada de Anotações Usando Descritores) (EN) está sendo considerada como uma possível solução, mas algum outro acordo ainda pode ser alcançado.
Resumindo: ler dicas de tipo durante a execução não é 100% confiável no Python 3.10 e provavelmente mudará em alguma futura versão.
|
Note
|
Empresas usando o Python em escala muito ampla desejam os benefícios da tipagem estática, mas não querem pagar o preço da avaliação de dicas de tipo no momento da importação. A checagem estática acontece nas estações de trabalho dos desenvolvedores e em servidores de integração contínua dedicados, mas o carregamento de módulos acontece em uma frequência e um volume muito mais altos, em servidores de produção, e esse custo não é desprezível em grande escala. Isso cria uma tensão na comunidade Python, entre aqueles que querem as dicas de tipo armazenadas apenas como strings—para reduzir os custos de carregamento—versus aqueles que também querem usar as dicas de tipo durante a execução, como os criadores e os usuários do pydantic e da FastAPI, para quem seria mais fácil acessar diretamente os tipos, ao invés de precisarem analisar strings nas anotações, uma tarefa desafiadora. |
Dada a instabilidade da situação atual, se você precisar ler anotações durante a execução, recomendo o seguinte:
-
Evite ler
__annotations__diretamente; em vez disso, useinspect.get_annotations(desde o Python 3.10) outyping.get_type_hints(desde o Python 3.5). -
Escreva uma função personalizada própria, como um invólucro para
inspect.get_annotationsoutyping.get_type_hints, e faça o restante de sua base de código chamar aquela função, de forma que mudanças futuras fiquem restritas a um único local.
Para demonstrar esse segundo ponto, aqui estão as primeiras linhas da classe Checked, definida no
[checked_class_top_ex], classe que estudaremos no [class_metaprog]:
class Checked:
@classmethod
def _fields(cls) -> dict[str, type]:
return get_type_hints(cls)
# ... more lines ...O método de Checked._fields evita que outras partes do módulo dependam diretamente de
typing.get_type_hints. Se get_type_hints mudar no futuro, exigindo lógica adicional, ou se eu quiser substituí-la por inspect.get_annotations, a mudança estará limitada a Checked._fields e não afetará o restante do programa.
|
Warning
|
Dadas as discussões correntes e as mudanças propostas para a inspeção de dicas de tipo durante a execução, a página da documentação oficial "Boas Práticas de Anotação" é uma leitura obrigatória, e a página deve ser atualizada até o lançamento do Python 3.11. Aquele how-to foi escrito por Larry Hastings, autor da PEP 649—Deferred Evaluation Of Annotations Using Descriptors (Avaliação Adiada de Anotações Usando Descritores) (EN), uma proposta alternativa para tratar os problemas gerados durante a execução pela PEP 563—Postponed Evaluation of Annotations (_Avaliação Adiada de Anotações) (EN). |
As seções restantes desse capítulo cobrem tipos genéricos, começando pela forma de definir uma classe genérica, que pode ser parametrizada por seus usuários.
No [ex_tombola_abc], definimos a ABC Tombola: uma interface para classes que funcionam como um recipiente para sorteio de bingo. A classe LottoBlower do [ex_lotto] é uma implementação concreta.
Vamos agora estudar uma versão genérica de LottoBlower, usada da forma que aparece no Exemplo 15.
link:code/15-more-types/lotto/generic_lotto_demo.py[role=include]-
Para instanciar uma classe genérica, passamos a ela um parâmetro de tipo concreto, como
intaqui. -
O Mypy irá inferir corretamente que
firsté umint… -
… e que
remainé umatuplede inteiros.
Além disso, o Mypy aponta violações do tipo parametrizado com mensagens úteis, como ilustrado no Exemplo 16.
link:code/15-more-types/lotto/generic_lotto_errors.py[role=include]-
Na instanciação de
LottoBlower[int], o Mypy marca ofloat. -
Na chamada
.load('ABC'), o Mypy explica porque umastrnão serve:str.__iter__devolve umIterator[str], masLottoBlower[int]exige umIterator[int].
O Exemplo 17 é a implementação.
link:code/15-more-types/lotto/generic_lotto.py[role=include]-
Declarações de classes genéricas muitas vezes usam herança múltipla, porque precisamos de uma subclasse de
Genericpara declarar os parâmetros de tipo formais—nesse caso,T. -
O argumento
itemsem__init__é do tipoIterable[T], que se tornaIterable[int]quando uma instância é declarada comoLottoBlower[int]. -
O método
loadé igualmente restrito. -
O tipo do valor devolvido
Tagora se tornaintem umLottoBlower[int]. -
Nenhuma variável de tipo aqui.
-
Por fim,
Tdefine o tipo dos itens natupledevolvida.
|
Tip
|
A seção "User-defined generic types" (Tipos genéricos definidos pelo usuário) (EN), na documentação do módulo |
Agora que vimos como implementar um classe genérica, vamos definir a terminologia para falar sobre tipos genéricos.
Aqui estão algumas definições que encontrei estudando genéricos:[15]
- Tipo genérico
-
Um tipo declarado com uma ou mais variáveis de tipo.
Exemplos:LottoBlower[T],abc.Mapping[KT, VT] - Parâmetro de tipo formal
-
As variáveis de tipo que aparecem em um declaração de tipo genérica.
Exemplo:KTeVTno último exemplo:abc.Mapping[KT, VT] - Tipo parametrizado
-
Um tipo declarado com os parâmetros de tipo reais.
Exemplos:LottoBlower[int],abc.Mapping[str, float] - Parâmetro de tipo real
-
Os tipos reais passados como parâmetros quando um tipo parametrizado é declarado.
Exemplo: ointemLottoBlower[int]
O próximo tópico é sobre como tornar os tipos genéricos mais flexíveis, introduzindo os conceitos de covariância, contravariância e invariância.
|
Note
|
Dependendo de sua experiência com genéricos em outras linguagens, essa pode ser a parte mais difícil do livro. O conceito de variância é abstrato, e uma apresentação rigorosa faria essa seção se parecer com páginas tiradas de um livro de matemática. Na prática, a variância é mais relevante para autores de bibliotecas que querem suportar novos tipos de contêineres genéricos ou fornecer uma API baseada em callbacks. Mesmo nesses casos, é possível evitar muita complexidade suportando apenas contêineres invariantes—que é quase só o que temos hoje na biblioteca padrão. Então, em uma primeira leitura você pode pular toda essa seção, ou ler apenas as partes sobre tipos invariantes. |
Já vimos o conceito de variância na [callable_variance_sec], aplicado a tipos genéricos Callable parametrizados. Aqui vamos expandir o conceito para abarcar tipo genéricos de coleções, usando uma analogia do "mundo real" para tornar mais concreto esse conceito abstrato.
Imagine uma cantina escolar que tenha como regra que apenas máquinas servindo sucos podem ser instaladas ali.[16] Máquinas de bebida genéricas não são permitidas, pois podem servir refrigerantes, que foram banidos pela direção da escola.[17]
Vamos tentar modelar o cenário da cantina com uma classe genérica BeverageDispenser, que pode ser parametrizada com o tipo de bebida..
Veja o Exemplo 18.
installlink:code/15-more-types/cafeteria/invariant.py[role=include]-
Beverage,Juice, eOrangeJuiceformam uma hierarquia de tipos. -
Uma declaração
TypeVarsimples. -
BeverageDispenseré parametrizada pelo tipo de bebida. -
installé uma função global do módulo. Sua dica de tipo faz valer a regra de que apenas máquinas de suco são aceitáveis.
Dadas as definições no Exemplo 18, o seguinte código é legal:
link:code/15-more-types/cafeteria/invariant.py[role=include]Entretanto, isso não é legal:
link:code/15-more-types/cafeteria/invariant.py[role=include]Uma máquina que serve qualquer Beverage não é aceitável, pois a cantina exige uma máquina especializada em Juice.
De forma um tanto surpreendente, este código também é ilegal:
link:code/15-more-types/cafeteria/invariant.py[role=include]Uma máquina especializada em OrangeJuice também não é permitida.
Apenas BeverageDispenser[Juice] serve.
No jargão da tipagem, dizemos que BeverageDispenser(Generic[T]) é invariante quando BeverageDispenser[OrangeJuice] não é compatível com BeverageDispenser[Juice]—apesar do fato de OrangeJuice ser um subtipo-de Juice.
Os tipos de coleções mutáveis do Python—tal como list e set—são invariantes.
A classe LottoBlower do Exemplo 17 também é invariante.
Se quisermos ser mais flexíveis, e modelar as máquinas de bebida como uma classe genérica que aceite alguma bebida e também seus subtipos, precisamos tornar a classe covariante.
O Exemplo 19 mostra como declararíamos BeverageDispenser.
install functionlink:code/15-more-types/cafeteria/covariant.py[role=include]-
Define
covariant=Trueao declarar a variável de tipo;_coé o sufixo convencional para parâmetros de tipo covariantes no typeshed. -
Usa
T_copara parametrizar a classe especialGeneric. -
As dicas de tipo para
installsão as mesmas do Exemplo 18.
O código abaixo funciona porque tanto a máquina de Juice quanto a de OrangeJuice são válidas em uma BeverageDispenser covariante:
link:code/15-more-types/cafeteria/covariant.py[role=include]mas uma máquina de uma Beverage arbitrária não é aceitável:
link:code/15-more-types/cafeteria/covariant.py[role=include]Isso é uma covariância: a relação de subtipo das máquinas parametrizadas varia na mesma direção da relação de subtipo dos parâmetros de tipo.
Vamos agora modelar a regra da cantina para a instalação de uma lata de lixo. Vamos supor que a comida e a bebida são servidas em recipientes biodegradáveis, e as sobras e utensílios descartáveis também são biodegradáveis. As latas de lixo devem ser adequadas para resíduos biodegradáveis.
|
Note
|
Neste exemplo didático, vamos fazer algumas suposições e classificar o lixo em uma hierarquia simplificada:
|
Para modelar a regra descrevendo uma lata de lixo aceitável na cantina, precisamos introduzir o conceito de "contravariância" através de um exemplo, apresentado no Exemplo 20.
installlink:code/15-more-types/cafeteria/contravariant.py[role=include]-
Uma hierarquia de tipos para resíduos:
Refuseé o tipo mais geral,Compostableo mais específico. -
T_contraé o nome convencional para uma variável de tipo contravariante. -
TrashCané contravariante ao tipo de resíduo. -
A função
deployexige uma lata de lixo compatível comTrashCan[Biodegradable].
Dadas essas definições, os seguintes tipos de lata de lixo são aceitáveis:
link:code/15-more-types/cafeteria/contravariant.py[role=include]A função deploy aceita uma TrashCan[Refuse], pois ela pode receber qualquer tipo de resíduo, incluindo Biodegradable.
Entretanto, uma TrashCan[Compostable] não serve, pois ela não pode receber Biodegradable:
link:code/15-more-types/cafeteria/contravariant.py[role=include]Vamos resumir os conceitos vistos até aqui.
A variância é uma propriedade sutil. As próximas seções recapitulam o conceito de tipos invariantes, covariantes e contravariantes, e fornecem algumas regras gerais para pensar sobre eles.
Um tipo genérico L é invariante quando não há nenhuma relação de supertipo ou subtipo entre dois tipos parametrizados, independente da relação que possa existir entre os parâmetros concretos.
Em outras palavras, se L é invariante, então L[A] não é supertipo ou subtipo de L[B].
Eles são inconsistentes em ambos os sentidos.
Como mencionado, as coleções mutáveis do Python são invariantes por default.
O tipo list é um bom exemplo:
list[int] não é consistente-com list[float], e vice-versa.
Em geral, se um parâmetro de tipo formal aparece em dicas de tipo de argumentos a métodos, e o mesmo parâmetro aparece nos tipos devolvidos pelo método, aquele parâmetro deve ser invariante, para garantir a segurança de tipo na atualização e leitura da coleção.
Por exemplo, aqui está parte das dicas de tipo para o tipo embutido list no
typeshed:
class list(MutableSequence[_T], Generic[_T]):
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, iterable: Iterable[_T]) -> None: ...
# ... lines omitted ...
def append(self, __object: _T) -> None: ...
def extend(self, __iterable: Iterable[_T]) -> None: ...
def pop(self, __index: int = ...) -> _T: ...
# etc...Veja que _T aparece entre os argumentos de __init__, append e extend,
e como tipo devolvido por pop.
Não há como tornar segura a tipagem dessa classe se ela for covariante ou contravariante em _T.
Considere dois tipos A e B, onde B é consistente-com A, e nenhum deles é Any.
Alguns autores usam os símbolos <: e :> para indicar relações de tipos como essas:
A :> B-
Aé um supertipo-de ou igual aB. B <: A-
Bé um subtipo-de ou igual aA.
Dado A :> B, um tipo genérico C é covariante quando C[A] :> C[B].
Observe que a direção da seta no símbolo :> é a mesma nos dois casos em que A está à esquerda de B.
Tipos genéricos covariantes seguem a relação de subtipo do tipo real dos parâmetros.
Contêineres imutáveis podem ser covariantes.
Por exemplo, é assim que a classe typing.FrozenSet está
documentada como covariante com uma variável de tipo usando o nome convencional T_co:
class FrozenSet(frozenset, AbstractSet[T_co]):Aplicando a notação :> a tipos parametrizados, temos:
float :> int
frozenset[float] :> frozenset[int]Iteradores são outro exemplo de genéricos covariantes:
eles não são coleções apenas para leitura como um frozenset,
mas apenas produzem saídas.
Qualquer código que espere um abc.Iterator[float] que produz números de ponto flutuante pode usar com segurança um abc.Iterator[int] que produz inteiros.
Tipos Callable são covariantes no tipo devolvido por uma razão similar.
Dado A :> B, um tipo genérico K é contravariante se K[A] <: K[B].
Tipos genéricos contravariantes revertem a relação de subtipo dos tipos reais dos parâmetros .
A classe TrashCan exemplifica isso:
Refuse :> Biodegradable
TrashCan[Refuse] <: TrashCan[Biodegradable]Um contêiner contravariante normalmente é uma estrutura de dados só para escrita, também conhecida como "coletor" ("sink"). Não há exemplos de coleções desse tipo na biblioteca padrão, mas existem alguns tipos com parâmetros de tipo contravariantes.
Callable[[ParamType, …], ReturnType] é contravariante nos tipos dos parâmetros, mas covariante no ReturnType, como vimos na [callable_variance_sec].
Além disso,
Generator,
Coroutine, e
AsyncGenerator
têm um parâmetro de tipo contravariante.
O tipo Generator está descrito na [generic_classic_coroutine_types_sec];
Coroutine e AsyncGenerator são descritos no [async_ch].
Para efeito da presente discussão sobre variância, o ponto principal é que parâmetros formais contravariantes definem o tipo dos argumentos usados para invocar ou enviar dados para o objeto, enquanto parâmetros formais covariantes definem os tipos de saídas produzidos pelo objeto—o tipo devolvido por uma função ou produzido por um gerador. Os significados de "enviar" e "produzir" são explicados na [classic_coroutines_sec].
Dessas observações sobre saídas covariantes e entradas contravariantes podemos derivar algumas orientações úteis.
Por fim, aqui estão algumas regras gerais a considerar quando estamos pensando sobre variância:
-
Se um parâmetro de tipo formal define um tipo para dados que saem de um objeto, ele pode ser covariante.
-
Se um parâmetro de tipo formal define um tipo para dados que entram em um objeto, ele pode ser contravariante.
-
Se um parâmetro de tipo formal define um tipo para dados que saem de um objeto e o mesmo parâmetro define um tipo para dados que entram em um objeto, ele deve ser invariante.
-
Na dúvida, use parâmetros de tipo formais invariantes. Não haverá prejuízo se no futuro precisar usar parâmetros de tipo covariantes ou contravariantes, pois nestes casos a tipagem é mais aberta e não quebrará códigos existentes.
Callable[[ParamType, …], ReturnType] demonstra as regras #1 e #2:
O ReturnType é covariante, e cada ParamType é contravariante.
Por default, TypeVar cria parâmetros formais invariantes, e é assim que as coleções mutáveis na biblioteca padrão são anotadas.
Nossa discussão sobre variância continua na [generic_classic_coroutine_types_sec].
A seguir, vamos ver como definir protocolos estáticos genéricos, aplicando a ideia de covariância a alguns novos exemplos.
A biblioteca padrão do Python 3.10 fornece alguns protocolos estáticos genéricos.
Um deles é SupportsAbs, implementado assim no
módulo typing:
@runtime_checkable
class SupportsAbs(Protocol[T_co]):
"""An ABC with one abstract method __abs__ that is covariant in its
return type."""
__slots__ = ()
@abstractmethod
def __abs__(self) -> T_co:
passT_co é declarado de acordo com a convenção de nomenclatura:
T_co = TypeVar('T_co', covariant=True)Graças a SupportsAbs, o Mypy considera válido o seguinte código, como visto no Exemplo 21.
SupportsAbslink:code/15-more-types/protocol/abs_demo.py[role=include]-
Definir
__abs__tornaVector2dconsistente-comSupportsAbs. -
Parametrizar
SupportsAbscomfloatassegura… -
…que o Mypy aceite
abs(v)como primeiro argumento paramath.isclose. -
Graças a
@runtime_checkablena definição deSupportsAbs, essa é uma asserção válida durante a execução. -
Todo o restante do código passa pelas verificações do Mypy e pelas asserções durante a execução.
-
O tipo
inttambém é consistente-comSupportsAbs. De acordo com o typeshed,int.__abs__devolve umint, o que é consistente-com o parametro de tipofloatdeclarado na dica de tipois_unitpara o argumentov.
De forma similar, podemos escrever uma versão genérica do protocolo RandomPicker, apresentado na [ex_randompick_protocol], que foi definido com um único método pick devolvendo Any.
O Exemplo 22 mostra como criar um RandomPicker genérico, covariante no tipo devolvido por pick.
RandomPicker genéricolink:code/15-more-types/protocol/random/generic_randompick.py[role=include]-
Declara
T_cocomocovariante. -
Isso torna
RandomPickergenérico, com um parâmetro de tipo formal covariante. -
Usa
T_cocomo tipo do valor devolvido.
O protocolo genérico RandomPicker pode ser covariante porque seu único parâmetro formal é usado em um tipo de saída.
Com isso, podemos dizer que temos um capítulo.
Começamos com um exemplo simples de uso de @overload, seguido por um exemplo muito mais complexo, que estudamos em detalhes:
as assinaturas sobrecarregadas exigidas para anotar corretamente a função embutida max.
A seguir veio o artefato especial da linguagem typing.TypedDict.
Escolhi tratar dele aqui e não no [data_class_ch], onde vimos typing.NamedTuple, porque TypedDict não é uma fábrica de classes; ele é meramente uma forma de acrescentar dicas de tipo a uma variável ou a um argumento que exige um dict com um conjunto específico de chaves do tipo string, e tipos específicos para cada chave—algo que acontece quando usamos um dict como registro, muitas vezes no contexto do tratamento de dados JSON.
Aquela seção foi um pouco mais longa porque usar TypedDict pode levar a um falso sentimento de segurança, e queria mostrar como as verificações durante a execução e o tratamento de erros são inevitáveis quando tentamos criar registros estruturados estaticamente a partir de mapeamentos, que por natureza são dinâmicos.
Então falamos sobre typing.cast, uma função projetada para nos permitir guiar o trabalho do verificador de tipos. É importante considerar cuidadosamente quando usar cast, porque seu uso excessivo atrapalha o verificador de tipos.
O acesso a dicas de tipo durante a execução veio em seguida. O ponto principal era usar typing.get_type_hints em vez de ler o atributo __annotations__ diretamente. Entretanto, aquela função pode não ser confiável para algumas anotações, e vimos que os desenvolvedores principais do Python ainda estão discutindo uma forma de tornar as dicas de tipo usáveis durante a execução, e ao mesmo tempo reduzir seu impacto sobre o uso de CPU e memória.
A última seção foi sobre genéricos, começando com a classe genérica LottoBlower—que mais tarde aprendemos ser uma classe genérica invariante.
Aquele exemplo foi seguido pelas definições de quatro termos básicos:
tipo genérico, parâmetro de tipo formal, tipo parametrizado e parâmetro de tipo real.
Continuamos pelo grande tópico da variância, usando máquinas bebidas para uma cantina e latas de lixo como exemplos da "vida real" para tipos genéricos invariantes, covariantes e contravariantes. Então revisamos, formalizamos e aplicamos aqueles conceitos a exemplos na biblioteca padrão do Python.
Por fim, vimos como é definido um protocolo estático genérico, primeiro considerando o protocolo typing.SupportsAbs, e então aplicando a mesma ideia ao exemplo do RandomPicker, tornando-o mais rigoroso que o protocolo original do [ifaces_prot_abc].
|
Note
|
O sistema de tipos do Python é um campo imenso e em rápida evolução. Este capítulo não é abrangente. Escolhi me concentrar em tópicos que são ou amplamente aplicáveis, ou particularmente complexos ou conceitualmente importantes, e que assim provavelmente se manterão relevantes por um longo tempo. |
O sistema de tipagem estática do Python já era complexo quando foi originalmente projetado, e tem se tornado mais complexo a cada ano. A Tabela 1 lista todas as PEPs que encontrei até maio de 2021. Seria necessário um livro inteiro para cobrir tudo.
typing. Pontos de interrogação na coluna Python indica PEPs em discussão ou ainda não implementadas; "n/a" aparece em PEPs informacionais sem relação com uma versão específica do Python. Todos os textos das PEPs estão em inglês. Dados coletados em maio 2021.
A documentação oficial do Python mal consegue acompanhar tudo aquilo, então a documentação do Mypy (EN) é uma referência essencial. Robust Python (EN), de Patrick Viafore (O’Reilly), é o primeiro livro com um tratamento abrangente do sistema de tipagem estática do Python que conheço, publicado em agosto de 2021. Você pode estar lendo o segundo livro sobre o assunto nesse exato instante.
O sutil tópico da variância tem sua própria seção na PEP 484 (EN), e também é abordado na página "Generics" (Genéricos) (EN) do Mypy, bem como em sua inestimável página "Common Issues" (Problemas Comuns).
A PEP 362—Function Signature Object (O Objeto Assinatura de Função)
vale a pena ler se você pretende usar o módulo inspect, que complementa a função typing.get_type_hints.
Se você estiver interessado na história do Python, pode gostar de saber que Guido van Rossum publicou "Adding Optional Static Typing to Python" (Acrescentando Tipagem Estática Opcional ao Python) em 23 de dezembro de 2004.
"Python 3 Types in the Wild: A Tale of Two Type Systems" (Os Tipos do Python 3 na Natureza: Um Conto de Dois Sistemas de Tipo) (EN) é um artigo científico de Ingkarat Rak-amnouykit e outros, do Rensselaer Polytechnic Institute e do IBM TJ Watson Research Center. O artigo avalia o uso de dicas de tipo em projetos de código aberto no GitHub, mostrando que a maioria dos projetos não as usam , e também que a maioria dos projetos que incluem dicas de tipo aparentemente não usam um verificador de tipos. Achei particularmente interessante a discussão das semânticas diferentes do Mypy e do pytype do Google, onde os autores concluem que eles são "essencialmente dois sistemas de tipos diferentes."
Dois artigos fundamentais sobre tipagem gradual são "Pluggable Type Systems" (Sistemas de Tipo Conectáveis) (EN), de Gilad Bracha, e "Static Typing Where Possible, Dynamic Typing When Needed: The End of the Cold War Between Programming Languages" (Tipagem Estática Quando Possível, Tipagem Dinâmica Quando Necessário: O Fim da Guerra Fria Entre Linguagens de Programação) (EN), de Eric Meijer e Peter Drayton.[18]
Aprendi muito lendo as partes relevantes de alguns livros sobre outras linguagens que implementam algumas das mesmas ideias:
-
Atomic Kotlin (EN), de Bruce Eckel e Svetlana Isakova (Mindview)
-
Effective Java, 3rd ed., (EN), de Joshua Bloch (Addison-Wesley)
-
Programming with Types: TypeScript Examples (EN), de Vlad Riscutia (Manning)
-
Programming TypeScript (EN), de Boris Cherny (O’Reilly)
-
The Dart Programming Language (EN) de Gilad Bracha (Addison-Wesley).[19]
Para algumas visões críticas sobre os sistemas de tipagem, recomendo os posts de Victor Youdaiken "Bad ideas in type theory" (Más ideias na teoria dos tipos) (EN) e "Types considered harmful II" (Tipos considerados nocivos II) (EN).
Por fim, me surpreeendi ao encontrar "Generics Considered Harmful" (Genéricos Considerados Nocivos), de Ken Arnold, um desenvolvedor principal do Java desde o início, bem como co-autor das primeiras quatro edições do livro oficial The Java Programming Language (Addison-Wesley)—junto com James Gosling, o principal criador do Java.
Infelizmente, as críticas de Arnold também se aplicam ao sistema de tipagem estática do Python. Quando leio as muitas regras e casos especiais das PEPs de tipagem, sou constantemente lembrado dessa passagem do post de Arnold:
O que nos traz ao problema que sempre cito para o C++: eu a chamo de "exceção de enésima ordem à regra de exceção". Ela soa assim: "Você pode fazer x, exceto no caso y, a menos que y faça z, caso em que você pode se…"
Felizmente, o Python tem uma vantagem crítica sobre o Java e o C++: um sistema de tipagem opcional. Podemos silenciar os verificadores de tipo e omitir as dicas de tipo quando se tornam muito incômodos.
As tocas de coelho da tipagem
Quando usamos um verificador de tipo, algumas vezes somos obrigados a descobrir e importar classes que não precisávamos conhecer, e que nosso código não precisa usar—exceto para escrever dicas de tipo. Tais classes não são documentadas, provavelmente porque são consideradas detalhes de implementação pelos autores dos pacotes. Aqui estão dois exemplos da biblioteca padrão.
Tive que vasculhar a imensa documentação do asyncio,
e depois navegar o código-fonte de vários módulos daquele pacote para descobrir a classe não-documentada
TransportSocket no módulo igualmente não documentado asyncio.trsock
só para usar cast() no exemplo do server.sockets, na Coerção de Tipo.
Usar socket.socket em vez de TransportSocket seria incorreto, pois esse último não é subtipo do primeiro, como explicitado em uma
docstring (EN) no código-fonte.
Caí em uma toca de coelho similar quando acrescentei dicas de tipo ao
[primes_procs_top_ex], uma demonstração simples de multiprocessing.
Aquele exemplo usa objetos SimpleQueue,
obtidos invocando multiprocessing.SimpleQueue().
Entretanto, não pude usar aquele nome em uma dica de tipo,
porque multiprocessing.SimpleQueue não é uma classe!
É um método vinculado da classe não documentada multiprocessing.BaseContext,
que cria e devolve uma instância da classe SimpleQueue,
definida no módulo não-documentado multiprocessing.queues.
Em cada um desses casos, tive que gastar algumas horas até encontrar a
classe não-documentada correta para importar, só para escrever uma única dica de tipo.
Esse tipo de pesquisa é parte do trabalho quando você está escrevendo um livro.
Mas se eu estivesse criando o código para uma aplicação,
provavelmente evitaria tais caças ao tesouro por causa de uma única linha,
e simplesmente colocaria # type: ignore.
Algumas vezes essa é a única solução com custo-benefício positivo.
Notação de variância em outras linguagens
A variância é um tópico complicado, e a sintaxe das dicas de tipo do Python não é tão boa quanto poderia ser. Essa citação direta da PEP 484 evidencia isso:
Covariância ou contravariância não são propriedaades de uma variável de tipo, mas sim uma propriedade da classe genérica definida usando essa variável.[20]
Se esse é o caso, por que a covariância e a contravarância são declaradas com TypeVar
e não na classe genérica?
Os autores da PEP 484 trabalharam sob a severa restrição auto-imposta de suportar dicas de tipo sem fazer qualquer modificação no interpretador.
Isso exigiu a introdução de TypeVar para definir variáveis de tipo,
e também levou ao abuso de [] para fornecer a sintaxe Klass[T] para genéricos—em vez da notação Klass<T> usada em outras linguagens populares, incluindo C#, Java, Kotlin e TypeScript.
Nenhuma dessas linguagens exige que variáveis de tipo seja declaradas antes de serem usadas.
Além disso, a sintaxe do Kotlin e do C# torna claro se um parâmetro de tipo é covariante, contravariante ou invariante exatamente onde isso faz sentido: na declaração de classe ou interface.
Em Kotlin, poderíamos declarar a BeverageDispenser assim:
class BeverageDispenser<out T> {
// etc...
}O modificador out no parâmetro de tipo formal significa que T é um tipo de
output (saída)), e portanto BeverageDispenser é covariante.
Você provavelmente consegue adivinhar como TrashCan seria declarada:
class TrashCan<in T> {
// etc...
}Dado T como um parâmetro de tipo formal de input (entrada),
segue que TrashCan é contravariante.
Se nem in nem out aparecem, então a classe é invariante naquele parâmetro.
É fácil lembrar das Regras gerais de variância quando out e in são usado nos parâmetros de tipo formais.
Isso sugere que uma boa convenção para nomenclatura de variáveis de tipo covariante e contravariantes no Python seria:
T_out = TypeVar('T_out', covariant=True)
T_in = TypeVar('T_in', contravariant=True)Aí poderíamos definir as classes assim:
class BeverageDispenser(Generic[T_out]):
...
class TrashCan(Generic[T_in]):
...Será que é tarde demais para modificar a convenção de nomenclatura definida na PEP 484?
json.loads() desde 2016, em Mypy issue #182: Define a JSON type (Definir um tipo JSON) (EN).
enumerate no exemplo serve para confundir intencionalmente o verificador de tipo. Uma implementação mais simples, produzindo strings diretamente, sem passar pelo índice de enumerate, seria corretamente analisada pelo Mypy, e o cast() não seria necessário.
sockets em asyncio.base_events.Server sockets attribute.", e ele foi rapidamente resolvido por Sebastian Rittau. Mas decidi manter o exemplo, pois ele ilustra um caso de uso comum para cast, e o cast que escrevi é inofensivo.
# type: ignore às linhas com `server.sockets[0]` porque, após pesquisar um pouco, encontrei linhas similares na documentação do asyncio e em um caso de teste (EN), e aí comecei a suspeitar que o problema não estava em meu código.
clip, mas se você tiver curiosidade, pode ler o módulo completo em clip_annot.py.