Aprendi uma dura lição: para programas pequenos, a tipagem dinâmica é ótima. Para programas grandes precisamos de 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
Este capítulo é uma continuação do [ch_type_hints_def], e fala mais sobre o sistema de tipagem gradual de Python. Os tópicos principais são:
-
Assinaturas de funções sobrecarregadas
-
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 sobrecargas.
No Python, as funções podem aceitar diferentes combinações de argumentos.
O decorador @typing.overload permite anotar tais combinações. Isto é
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), traduzido:
>>> 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').
Esta 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
sobrecarregadas 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 sobrecargas.
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, em vez usar 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 é checada pelo Mypy. Isso significa que você pode invocar sum(my_list),
mas não sum(__iterable = my_list).
O checador de tipos tenta fazer a correspondência entre os argumentos dados com
cada assinatura sobrecarregada, em ordem. A chamada sum(range(100), 1000) não
casa com a primeira sobrecarga, pois aquela assinatura tem apenas um parâmetro.
Mas casa com a segunda.
Você pode também usar @overload em um modulo Python (.py) normal, colocando
as assinaturas sobrecarregadas logo antes da assinatura real da função e de sua
implementação. O mysum.py: definição da função sum com assinaturaas sobrecarregadas mostra como sum apareceria anotada e
implementada em um módulo Python.
sum com assinaturaas sobrecarregadaslink:../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 real da função não tem dicas de tipo.
São muitas linhas para anotar uma função de uma única linha.
Sinto muito, mas pelo menos a função do exemplo não é foo.
Se 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 de Python tem 186 sobrecargas—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 inconvenientes para quem vai usar. 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
sobrecargas para anotar adequadamente a função embutida max,
que é muito flexível nos parâmetros que aceita.
É difícil acrescentar dicas de tipo a funções que usam os poderosos recursos dinâmicos de Python.
Quando estudava o typeshed, encontrei o relatório de bug
#4051: Mypy não avisou que é proibido 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'
Tradução: '>' 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 evidenciar a relação entre o
funcionamento da função e as anotações sobrecarregadas (a função embutida
original é escrita em C); veja o mymax.py: Versão da função max em Python.
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 deste exemplo não é a lógica de max, então não vou explicar a
implementação, exceto para falar sobre 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 estas duas situações:
-
O usuário passou
Nonecomo argumentodefault. -
O usuário não passou o argumento
default(neste caso seu valor fica sendoMISSING).
Quando first é um iterável vazio…
-
Se o usuário não forneceu um argumento para
default=, então ele éMISSING, emaxgera umValueError. -
Se usuário forneceu um valor para
default=, incluindoNone, e entãomaxdevolve o valor dedefault.
Para consertar o issue #4051, escrevi o código no mymax.py: início do módulo, com importações, definições e sobrecargas.[2]
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 à tipagem pato, meu código
não tem nenhuma checagem usando isinstance, e fornece a mesma checagem de erro
daquelas dicas de tipo—mas apenas durante a execução, claro.
Uma vantagem importante de @overload é declarar o tipo devolvido da forma
mais precisa possível, de acordo com os tipos dos argumentos recebidos. Veremos
este vantagem a seguir, estudando as sobrecargas 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:
...Nestes 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 sobrecargas:
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 sobrecargas:
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 sobrecargas:
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, escrever tantas linhas para suportar o checador de tipos
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 sobrecarga—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 à de 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, como dados em formato JSON.
|
Warning
|
É tentador usar |
Algumas vezes os dicionários de 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 de 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]-
As chaves são
strmas os valores podem ser de qualquer tipo. dict[str, 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_) resolve este problema. O books.py: a definição de BookDict 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 [ch_dataclass].
A similaridade sintática é enganosa. TypedDict é muito diferente. Ele existe
apenas para orientar um checador de tipos, e não tem qualquer efeito
durante a execução.
TypedDict fornece duas coisas:
-
Uma sintaxe similar à de classe para anotar um
dictcom dicas de tipo para os valores de cada campo identificado por um chaves. -
Um construtor que informa que o checador de tipos deve 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ção da pseudoclasse não criam atributos de instância.
-
Não é possível escrever inicializadores com valores default para os "campos".
-
Não é permitido definir métodos.
Vamos explorar o comportamento de um BookDict durante a execução (no
Usando um BookDict, mas não exatamente como planejado).
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. -
Ops… esqueci que
authorsdeve ser uma lista. Mas não há checagem de tipos estáticos 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 checador de tipos, 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 [ch_dataclass], por outro lado,
são úteis mesmo se você não usar um checador de tipos,
porque durante a execução elas geram uma classe customizada que pode ser instanciada.
Elas também fornecem vários métodos ou funções úteis,
listadas na [dc_main_features_sec].
O demo_books.py: operações legais e ilegais em um BookDict cria um BookDict válido e tenta executar algumas operações com ele.
A seguir, o Verificando os tipos em demo_books.py 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 checados. 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 de 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 Verificando os tipos em demo_books.py. -
As últimas três linhas da função
demosão ilegais. Elas vão disparar mensagens de erro no Verificando os tipos em demo_books.py.
Verificando a tipagem em demo_books.py, do demo_books.py: operações legais e ilegais em um BookDict, obtemos o
Verificando os tipos em demo_books.py.
…/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)-
Esta 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]. Checadores de tipo em geral não permitem que o tipo de uma variável mude.[3] -
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 o
books.py: a função to_xml.[4]
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 como inferir o tipo dos elementos. Por isso a anotação de tipo é necessária aqui.[5]
-
O Mypy entende testes com
isinstance, e tratavaluecomo umalistneste bloco. -
Quando usei
key == 'authors'como condição doifque guarda este 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é umalistneste bloco.
O books_any.py: a função from_json 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.[6] -
Posso devolver
whatever—de tipoAny—porqueAnyé consistente-com todos os tipos, incluindo o tipo declarado do valor devolvido,BookDict.
É muito importante de ter em mente segundo ponto do books_any.py: a função from_json:
o Mypy não vai apontar qualquer problema neste código, mas durante a execução
o valor em whatever pode não se adequar à estrutura de BookDict—pode até
não 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 books.py: a função from_json com uma anotação de variável.
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
books.py: a função |
A checagem de tipos 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 demo_not_book.py: from_json devolve um BookDict inválido, e to_xml o aceita, o
Relatório do Mypy para demo_not_book.py, reformatado por legibilidade e o Resultado da execução de demo_not_book.py 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 acontecerá? -
Lembre-se da assinatura:
to_xml(book: BookDict) {rt-arrow} str: -
Como será a saída em XML?
Agora checamos demo_not_book.py com o Mypy:
…/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 estático, não o conteúdo de
not_bookdurante a execução. -
De novo, este é o tipo estático de
not_book['authors'], como definido emBookDict. Não o tipo durante a execução. -
Este erro é para a linha
print(not_book['flavor']): esta chave não existe no tipo estático.
Agora vamos executar demo_not_book.py, mostrando o resultado no
Resultado da execução de demo_not_book.py.
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 checagem durante a execução: entra lixo, sai lixo.
O Resultado da execução de demo_not_book.py 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 books.py: a função to_xml do ponto de vista da tipagem pato,
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.
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, estude 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).
Vamos agora voltar nossa atenção para uma função que é melhor evitar, mas que
algumas vezes é inevitável: typing.cast.
Nenhum sistema de tipos é perfeito, nem tampouco os checadores estáticos de tipo, as dicas de tipo no projeto typeshed ou as dicas de tipo em pacotes de terceiros, quando existem.
A função especial typing.cast() é 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 explica (tradução nossa):
Coerções são usadas para silenciar avisos espúrios do checador de tipos, e ajudam o checador quando ele não consegue entender o que está acontecendo.
Durante a execução, typing.cast não faz absolutamente nada.
Esta é 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 docstring diz: "Coage um valor para um tipo. Isto devolve o valor inalterado. Para o checador de tipos, isto sinaliza que o valor devolvido tem o tipo designado, mas na execução não fazemos nenhuma checagem (queremos que isto seja tão rápido quanto possível)".
A PEP 484 exige que os checadores de tipos "acreditem cegamente" em cast. A
seção "Casts" (Coerções) da PEP 484 mostra um exemplo
onde o checador 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 o índice de um item
str ou levantar 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 orientar o Mypy.[7]
Aqui está outro exemplo com cast, desta vez para corrigir uma dica de tipo
desatualizada na biblioteca padrão de Python. No [ex_tcp_mojifinder_main] do [ch_async], criei
um objeto asyncio.Server, e queria obter o endereço onde o servidor está
ouvindo (aceitando conexões). Escrevi esta 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 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,[8] 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.[9] Mas agora o código está mais
seguro.
A leitora atenta pode ter notado que sockets[0] poderia gerar um IndexError
se sockets estiver vazio.
Entretanto, até onde entendo o asyncio, isso não pode acontecer no
[ex_tcp_mojifinder_main] do [ch_async], 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 se acostume a usar |
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 ocasional chamada a
cast()ou um comentário# type: ignore?[10]
É insensato banir inteiramente o uso de cast, principalmente porque as
alternativas para contornar esses problemas são piores:
-
# type: ignoreé menos informativo.[11] -
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 checador de tipos 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 aqui ou ali, 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, 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 no
clipannot.py: a assinatura anotada da função clip.[12]
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 {rt-arrow} no clipannot.py: a assinatura anotada da função clip.
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' e '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 há dicas de tipo.
-
Referências a tipos ainda não definidos exigem o uso de strings em vez dos 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__. Quando uma empresa tem milhares de servidores importanto
arquivos Python, o custo pode ser significativo, mesmo considerando que a
importação de cada módulo só acontece no início do processo.
Vamos nos concentrar agora no segundo problema.
Armazenar anotações como string é necessário algumas vezes, por causa do problema da referência futura (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 futura: quando um método devolve um novo objeto da mesma classe. Já que o objeto classe não está definido até 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 futuras como strings é a prática padrão e exigida no Python 3.10. Os checadores de tipos 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 como
Auxiliares de introspecção,
sendo typing.get_type_hints a mais importante delas. 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 futuras 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) 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:
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 de 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 clipannot.py: a assinatura anotada da função clip
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 clipannot.py: a assinatura anotada da função clip).
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 reais—mesmo em alguns casos onde
a dica de tipo original foi escrita como uma string. Esta é 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 avisaram de que tal 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 e-mail 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 futuras, 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.[13]
O Steering Council de Python decidiu adiar a elevação da PEP 563 a comportamento padrão até 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_) 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 Python em escala muito grande 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 com uma frequência e um volume muito maiores, em servidores de produção, e este custo não é desprezível em grande escala. Isto 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 complicada. |
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 customizada 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,
que estudaremos no [ex_checked_class_top] do [ch_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 de 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), 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). |
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] do [ch_ifaces_prot_abc]
definimos a ABC Tombola: uma interface para classes que funcionam como um recipiente para sorteio de bingo. A classe LottoBlower ([ex_lotto] do [ch_ifaces_prot_abc]) é uma implementação concreta.
Vamos agora estudar uma versão genérica de LottoBlower, usada da forma que aparece no generic_lotto_demo.py: usando uma classe genérica de sorteio de bingo.
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 generic_lotto_errors.py: erros apontados pelo Mypy.
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 generic_lotto.py: uma classe genérica de sorteador de bingo é 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 incluir a superclasse
Genericpara declarar os parâmetros de tipo formais—neste 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 anotado. -
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), 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:[14]
- 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, esta pode ser a seção 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 de um livro de matemática. Na prática, a variância é mais relevante para autores de bibliotecas que querem suportar novos tipos de coleções genéricas ou fornecer uma API baseada em callbacks. Mesmo nestes casos, é possível evitar muita complexidade suportando apenas coleções invariantes—que é o que temos hoje na biblioteca padrão. Então, em uma primeira leitura você pode pular toda esta 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 tipos 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.[15] Máquinas de bebida genéricas não são permitidas, pois podem servir refrigerantes, que foram banidos pela direção da escola.[16]
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 invariant.py: definições de tipo e função install.
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 invariant.py: definições de tipo e função install, o seguinte código é válido:
link:../code/15-more-types/cafeteria/invariant.py[role=include]Entretanto, isto não é válido:
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 é inválido:
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 de Python—tal como list e set—são invariantes.
A classe LottoBlower do generic_lotto.py: uma classe genérica de sorteador de bingo 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 covariant.py: definições de tipos e função install mostra como declararíamos BeverageDispenser.
installlink:../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 invariant.py: definições de tipo e funçãoinstall.
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 contravariant.py: definições de tipo e a função install.
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 de 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 parâmetros 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 itens sob demanda.
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 pela mesma razão.
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 deste 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 [ch_async].
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 precisos de "enviar" e "produzir" são definidos na [classic_coroutines_sec].
A partir destas 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 ficará mais tolerante 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.
Veremos mais exemplos de variância em [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 de 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 abs_demo.py: uso do protocolo genérico SupportsAbs.
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 checagens 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 no [ex_randompick_protocol] do [ch_ifaces_prot_abc], que foi definido com
um único método pick devolvendo Any.
O generic_randompick.py: definição do RandomPicker genérico 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 mais um capítulo.
Começamos com um exemplo simples de
uso de @overload, seguido por um exemplo mais complexo, que estudamos em
detalhes: as assinaturas sobrecarregadas exigidas para anotar corretamente a
função embutida max.
A seguir veio o tipo especial typing.TypedDict. Escolhi tratar dele aqui e não
no [ch_dataclass], onde vimos typing.NamedTuple, porque TypedDict parece
mas 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 eu queria mostrar
como as checagens durante a execução e o tratamento de erros são inevitáveis
quando tentamos criar registros estruturados estaticamente a partir de
mapeamentos, que são dinâmicos por natureza.
Então falamos sobre typing.cast, uma função criada para nos permitir orientar
o checador de tipos. É importante considerar cuidadosamente quando usar cast,
porque seu uso excessivo atrapalha o checador 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 mantenedores de 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 e latas de lixo para uma cantina 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 de 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 [ch_ifaces_prot_abc].
|
Note
|
O sistema de tipos de Python é um campo imenso e em rápida expansão. Este capítulo não é abrangente. Escolhi me concentrar em tópicos que são amplamente aplicáveis, ou particularmente complexos, ou conceitualmente importantes, e que provavelmente serão relevantes por mais tempo. |
O sistema de tipagem estática de Python
já era complexo quando foi originalmente projetado, e tem se tornado mais complexo a cada ano.
A PEPs sobre dicas de tipo, com links nos títulos. PEPs com números marcados com * são importantes o suficiente para serem mencionadas no parágrafo de abertura da documentação de 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 de Python. Dados coletados em maio de 2021. 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 de Python. Dados coletados em maio de 2021.
| PEP | título | Python | ano |
|---|---|---|---|
3107 |
Function Annotations (Anotações de Função) |
3.0 |
2006 |
483* |
The Theory of Type Hints (A Teoria das Dicas de Tipo) |
n/a |
2014 |
484* |
Type Hints (Dicas de Tipo) |
3.5 |
2014 |
482 |
Literature Overview for Type Hints (Revisão da Literatura sobre Dicas de Tipo) |
n/a |
2015 |
526* |
Syntax for Variable Annotations (Sintaxe para Anotações de Variáveis) |
3.6 |
2016 |
544* |
Protocols: Structural subtyping (static duck typing) (Protocolos: subtipagem estrutural (duck typing estático)) |
3.8 |
2017 |
557 |
Data Classes (Classes de Dados) |
3.7 |
2017 |
560 |
Core support for typing module and generic types (Suporte nativo para tipagem de módulos e tipos genéricos) |
3.7 |
2017 |
561 |
Distributing and Packaging Type Information (Distribuindo e Empacotando Informação de Tipo) |
3.7 |
2017 |
563 |
Postponed Evaluation of Annotations (Avaliação Adiada de Anotações) |
3.7 |
2017 |
586* |
Literal Types (Tipos Literais) |
3.8 |
2018 |
585 |
Type Hinting Generics In Standard Collections (Dicas de Tipo para Genéricos nas Coleções Padrão) |
3.9 |
2019 |
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) |
3.8 |
2019 |
591* |
Adding a final qualifier to typing (Acrescentando um qualificador final à tipagem) |
3.8 |
2019 |
593 |
Flexible function and variable annotations (Anotações flexíveis para funções e variáveis) |
? |
2019 |
604 |
Allow writing union types as X | Y (Permitir a definição de tipos de união como X | Y) |
3.10 |
2019 |
612 |
Parameter Specification Variables (Variáveis de Especificação de Parâmetros) |
3.10 |
2019 |
613 |
Explicit Type Aliases (Aliases de Tipo Explícitos) |
3.10 |
2020 |
645 |
Allow writing optional types as x? (Permitir a definição de tipos opcionais como x?) |
? |
2020 |
646 |
Variadic Generics (Genéricos Variádicos) |
? |
2020 |
647 |
User-Defined Type Guards (Guardas de Tipos Definidos pelo Usuário) |
3.10 |
2021 |
649 |
Deferred Evaluation Of Annotations Using Descriptors (Avaliação Adiada de Anotações Usando Descritores) |
? |
2021 |
655 |
Marking individual TypedDict items as required or potentially-missing (Marcando itens individuais de TypedDict como obrigatórios ou potencialmente ausentes) |
? |
2021 |
A documentação oficial de Python mal consegue acompanhar tudo aquilo, então a documentação do Mypy é uma referência essencial. Robust Python, de Patrick Viafore (O’Reilly), é o primeiro livro com um tratamento abrangente do sistema de tipagem estática de Python que conheço, publicado em agosto de 2021. Você pode estar lendo o segundo livro sobre o assunto nesse exato instante.
O tópico sutil da variância tem sua própria seção na PEP 484, e também é abordado na página Generics 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 tiver interesse na história de Python, saiba que Guido van Rossum publicou Adding Optional Static Typing to Python (Acrescentando tipagem estática opcional ao Python).
Python 3 Types in the Wild: A Tale of Two Type Systems (Os tipos de Python 3 na natureza: um conto de dois sistemas de tipo) é 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 usa, e também que a maioria dos projetos que têm dicas de tipo aparentemente não usa um checador 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), 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), de Eric Meijer e Peter Drayton.[17]
Aprendi muito lendo as partes relevantes de alguns livros sobre outras linguagens que implementam algumas das mesmas ideias:
-
Atomic Kotlin, de Bruce Eckel e Svetlana Isakova (Mindview)
-
Effective Java, 3rd ed.,, de Joshua Bloch (Addison-Wesley)
-
Programming with Types: TypeScript Examples, de Vlad Riscutia (Manning)
-
Programming TypeScript, de Boris Cherny (O’Reilly)
-
The Dart Programming Language de Gilad Bracha (Addison-Wesley).[18]
Para algumas visões críticas sobre os sistemas de tipagem, recomendo os posts de Victor Youdaiken Bad ideas in type theory (Ideias ruins em teoria dos tipos) e Types considered harmful II (Tipos considerados nocivos II).
Por fim, me surpreendi ao encontrar Generics Considered Harmful (Genéricos Considerados Nocivos), de Ken Arnold, um desenvolvedor principal de Java desde o início, bem como co-autor das primeiras quatro edições do livro oficial The Java Programming Language (Addison-Wesley)—com James Gosling, o principal criador de Java.
Infelizmente, as críticas de Arnold também se aplicam ao sistema de tipagem estática de 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++: a "exceção de enésima ordem à regra de exceção".
É mais ou menos assim: "Você pode fazer x, exceto no caso y, a menos que y faça z, caso em que você pode se…"
Felizmente, Python tem uma vantagem crítica sobre o Java e o C++: um sistema de tipagem opcional. Podemos silenciar os checadores de tipos e omitir as dicas de tipo quando se tornam muito inconvenientes.
As tocas de coelho da tipagem
Quando usamos um checador de tipos, 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 (type casting). Usar socket.socket em vez de TransportSocket seria
incorreto, pois esse último não é subtipo do primeiro, como explicitado em uma
docstring no código-fonte.
Caí em uma toca de coelho similar quando acrescentei dicas de tipo ao
[ex_primes_procs_top] do [ch_concurrency_models], 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
Notação de variância em outras linguagens
A variância é um tópico complicado, e a sintaxe das dicas de tipo de Python deixa a desejar. Esta citação direta da PEP 484 evidencia isso:
Covariância ou contravariância não são propriedades de uma variável de tipo, mas sim uma propriedade da classe genérica definida usando essa variável.[19]
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 optaram por introduzir dicas de tipo sem fazer
qualquer modificação no interpretador.
Em Python, todo identificador aparece pela primeira vez no código-fonte
de um módulo através de uma
atribuição, ou uma instrução especial como import, class, ou def.
Por isso tiveram que criar TypeVar para declarar uma variável de tipo
através de uma atribuição:
T = TypeVar('T')Para não mexer no parser, reutilizaram o operador [] na sintaxe
Klass[T] para genéricos—em vez da
notação Klass<T> usada em outras linguagens populares, incluindo C#, Java,
Kotlin e TypeScript. Estas linguagens não exigem que variáveis de tipo sejam
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),
então 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 (Regras gerais de variância)
quando out e in são usados nos parâmetros de tipo formais.
Isso sugere uma convenção melhor para nomear de variáveis de tipo
covariantes e contravariantes:
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á tarde demais para adotar T_out e T_in em vez de
T_co e T_contra que foram sugeridos na PEP 484?
json.loads() desde 2016, em Mypy issue #182: Define a JSON type (Definir um tipo JSON).
enumerate no exemplo serve para confundir intencionalmente o checador de tipos. 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, e aí comecei a suspeitar que o problema não estava no meu código.
# type:
ignore[code] permite especificar qual erro do Mypy está sendo silenciado, mas os códigos nem sempre são fáceis de interpretar. Veja a página Error codes na documentação do Mypy.
clip, mas se você tiver curiosidade, pode ler o módulo completo em clip_annot.py.