É preciso enfatizar que Python continuará sendo uma linguagem de tipagem dinâmica, e os autores não tem qualquer intenção de algum dia tornar dicas de tipo obrigatórias, mesmo que por mera convenção.
Guido van Rossum, Jukka Lehtosalo, e Łukasz Langa, PEP 484—Type Hints PEP 484—Type Hints (EN), "Rationale and Goals"; negritos mantidos do original.
Dicas de tipo foram a maior mudança na história do Python desde a unificação de tipos e classes no Python 2.2, lançado em 2001. Entretanto, as dicas de tipo não beneficiam igualmente a todos as pessoas que usam Python. Por isso deverão ser sempre opcionais.
A PEP 484—Type Hints introduziu a sintaxe e a semântica para declarações explícitas de tipo em argumentos de funções, valores de retorno e variáveis. O objetivo é ajudar ferramentas de desenvolvimento a encontrarem bugs nas bases de código em Python através de análise estática, isto é, sem precisar efetivamente executar o código através de testes.
Os maiores beneficiários são engenheiros de software profissionais que usam IDEs (Ambientes de Desenvolvimento Integrados) e CI (Integração Contínua). A análise de custo-benefício que torna as dicas de tipo atrativas para esse grupo não se aplica a todos os usuários de Python.
A base de usuários de Python vai muito além dessa classe de profissionais. Ela inclui cientistas, comerciantes, jornalistas, artistas, inventores, analistas e estudantes de inúmeras áreas — entre outros. Para a maioria deles, o custo de aprender dicas de tipo será certamente maior — a menos que já conheçam uma outra linguagem com tipos estáticos, subtipos e tipos genéricos. Os benefícios serão menores para muitos desses usuários, dada a forma como que eles interagem com Python, o tamanho menor de suas bases de código e de suas equipes — muitas vezes "equipes de um".
A tipagem dinâmica, default do Python, é mais simples e mais expressiva quando estamos escrevendo programas para explorar dados e ideias, como é o caso em ciência de dados, computação criativa e para aprender.
Este capítulo se concentra nas dicas de tipo de Python nas assinaturas de função.
[more_types_ch] explora as dicas de tipo no contexto de classes e outros recursos do módulo typing
.
Os tópicos mais importantes aqui são:
-
Uma introdução prática à tipagem gradual com Mypy
-
As perspectivas complementares da duck typing (tipagem pato) e da tipagem nominal
-
A revisão para principais categorias de tipos que podem surgir em anotações — isso representa cerca de 60% do capítulo
-
Os parâmetros variádicos das dicas de tipo (
*args
,**kwargs
) -
As limitações e desvantagens das dicas de tipo e da tipagem estática.
Este capítulo é completamente novo. As dicas de tipo apareceram no Python 3.5, após eu ter terminado de escrever a primeira edição de Python Fluente.
Dadas as limitações de um sistema de tipagem estática, a melhor ideia da PEP 484 foi propor um sistema de tipagem gradual. Vamos começar definindo o que isso significa.
A PEP 484 introduziu no Python um sistema de tipagem gradual. Outras linguagens com sistemas de tipagem gradual são o Typescript da Microsoft, Dart (a linguagem do SDK Flutter, criado pelo Google), e o Hack (um dialeto de PHP criado para uso na máquina virtual HHVM do Facebook). O próprio verificador de tipo MyPy começou como uma linguagem: um dialeto de Python de tipagem gradual com seu próprio interpretador. Guido van Rossum convenceu o criador do MyPy, Jukka Lehtosalo, a transformá-lo em uma ferramenta para checar código Python anotado.
Eis uma função com anotações de tipos:
def tokenize(s: str) -> list[str]:
"Convert a string into a list of tokens."
return s.replace('(', ' ( ').replace(')', ' ) ').split()
A assinatura informa que a função tokenize
recebe uma str
e devolve list[str]
: uma lista de strings.
A utilidade dessa função será explicada no [lis_parser_ex].
Um sistema de tipagem gradual:
- É opcional
-
Por default, o verificador de tipo não deve emitir avisos para código que não tenha dicas de tipo. Em vez disso, o verificador supõe o tipo
Any
quando não consegue determinar o tipo de um objeto. O tipoAny
é considerado compatível com todos os outros tipos. - Não captura erros de tipagem durante a execução do código
-
Dicas de tipo são usadas por verificadores de tipo, analisadores de código-fonte (linters) e IDEs para emitir avisos. Eles não evitam que valores inconsistentes sejam passados para funções ou atribuídos a variáveis durante a execução. Por exemplo, nada impede que alguém chame
tokenie(42)
, apesar da anotação de tipo do argumentos: str
). A chamada ocorrerá, e teremos um erro de execução no corpo da função. - Não melhora o desempenho
-
Anotações de tipo fornecem dados que poderiam, em tese, permitir otimizações do bytecode gerado. Mas, até julho de 2021, tais otimizações não ocorrem em nenhum ambiente Python que eu conheça.[1]
O melhor aspecto de usabilidade da tipagem gradual é que as anotações são sempre opcionais.
Nos sistemas de tipagem estáticos, a maioria das restrições de tipo são fáceis de expressar, muitas são desajeitadas, muitas são difíceis e algumas são impossíveis: Por exemplo, em julho de 2021, tipos recursivos não tinham suporte — veja as questões #182, Define a JSON type (EN) sobre o JSON e #731, Support recursive types (EN) do MyPy.
É perfeitamente possível que você escreva um ótimo programa Python, que consiga passar por uma boa cobertura de testes, mas ainda assim não consiga acrescentar dicas de tipo que satisfaçam um verificador de tipagem. Não tem problema; esqueça as dicas de tipo problemáticas e entregue o programa!
Dicas de tipo são opcionais em todos os níveis: você pode criar ou usar pacotes inteiros sem dicas de tipo, pode silenciar o verificador ao importar um daqueles pacotes sem dicas de tipo para um módulo onde você use dicas de tipo, e você também pode adicionar comentários especiais, para fazer o verificador de tipos ignorar linhas específicas do seu código.
Tip
|
Tentar impor uma cobertura de 100% de dicas de tipo irá provavelmente estimular seu uso de forma impensada, apenas para satisfazer essa métrica. Isso também vai impedir equipes de aproveitarem da melhor forma possível o potencial e a flexibilidade do Python. Código sem dicas de tipo deveria ser aceito sem objeções quando anotações tornassem o uso de uma API menos amigável ou quando complicassem em demasia seu desenvolvimento. |
Vamos ver como a tipagem gradual funciona na prática, começando com uma função simples e acrescentando gradativamente a ela dicas de tipo, guiados pelo Mypy.
Note
|
Há muitos verificadores de tipo para Python compatíveis com a PEP 484, incluindo o pytype do Google, o Pyright da Microsoft, o Pyre do Facebook — além de verificadores incluídos em IDEs como o PyCharm. Eu escolhi usar o Mypy nos exemplos por ele ser o mais conhecido. Entretanto, algum daqueles outros pode ser mais adequado para alguns projetos ou equipes. O Pytype, por exemplo, foi projetado para lidar com bases de código sem nenhuma dica de tipo e ainda assim gerar recomendações úteis. Ele é mais tolerante que o MyPy, e consegue também gerar anotações para o seu código. |
Vamos anotar uma função show_count
, que retorna uma string com um número e uma palavra no singular ou no plural, dependendo do número:
link:code/08-def-type-hints/messages/no_hints/messages.py[role=include]
Exemplo 1 mostra o código-fonte de show_count
, sem anotações.
show_count
de messages.py sem dicas de tipo.link:code/08-def-type-hints/messages/no_hints/messages.py[role=include]
Para começar a verificação de tipo, rodamos o comando mypy
passando o módulo messages.py como parâmetro:
…/no_hints/ $ pip install mypy
[muitas mensagens omitidas...]
…/no_hints/ $ mypy messages.py
Success: no issues found in 1 source file
Na configuração default, o Mypy não encontra nenhum problema com o Exemplo 1.
Warning
|
Durante a revisão deste capítulo estou usando Mypy 0.910, a versão mais recente no momento (em julho de 2021). A "Introduction" (EN) do Mypy adverte que ele "é oficialmente software beta. Mudanças ocasionais irão quebrar a compatibilidade com versões mais antigas." O Mypy está gerando pelo menos um relatório diferente daquele que recebi quando escrevi o capítulo, em abril de 2020. E quando você estiver lendo essas linhas, talvez os resultados também sejam diferentes daqueles mostrados aqui. |
Se a assinatura de uma função não tem anotações, Mypy a ignora por default — a menos que seja configurado de outra forma.
O Exemplo 2 também inclui testes de unidade do pytest
.
Este é código de messages_test.py.
link:code/08-def-type-hints/messages/no_hints/messages_test.py[role=include]
Agora vamos acrescentar dicas de tipo, guiados pelo Mypy.
A opção de linha de comando --disallow-untyped-defs
faz o Mypy apontar todas as definições de função que não tenham dicas de tipo para todos os argumentos e para o valor de retorno.
Usando --disallow-untyped-defs
com o arquivo de teste produz três erros e uma observação:
…/no_hints/ $ mypy --disallow-untyped-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
messages_test.py:10: error: Function is missing a type annotation
messages_test.py:15: error: Function is missing a return type annotation
messages_test.py:15: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files (checked 1 source file)
Nas primeiras etapas da tipagem gradual, prefiro usar outra opção:
--disallow-incomplete-defs
.
Inicialmente o Mypy não me dá nenhuma nova informação:
…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
Success: no issues found in 1 source file
Agora vou acrescentar apenas o tipo do retorno a show_count
em messages.py:
def show_count(count, word) -> str:
Isso é suficiente para fazer o Mypy olhar para o código. Usando a mesma linha de comando anterior para verificar messages_test.py fará o Mypy examinar novamente o messages.py:
…/no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
for one or more arguments
Found 1 error in 1 file (checked 1 source file)
Agora posso gradualmente acrescentar dicas de tipo, função por função, sem receber avisos sobre as funções onde ainda não adicionei anotações Essa é uma assinatura completamente anotada que satisfaz o Mypy:
def show_count(count: int, word: str) -> str:
Tip
|
Em vez de digitar opções de linha de comando como [mypy] python_version = 3.9 warn_unused_configs = True disallow_incomplete_defs = True |
A função show_count
no Exemplo 1 só funciona com substantivos regulares. Se o plural não pode ser composto acrescentando um 's'
, devemos deixar o usuário fornecer a forma plural, assim:
>>> show_count(3, 'mouse', 'mice')
'3 mice'
Vamos experimentar um pouco de "desenvolvimento orientado a tipos." Primeiro acrescento um teste usando aquele terceiro argumento. Não esqueça de adicionar a dica do tipo de retorno à função de teste, senão o Mypy não vai inspecioná-la.
def test_irregular() -> None:
got = show_count(2, 'child', 'children')
assert got == '2 children'
O Mypy detecta o erro:
…/hints_2/ $ mypy messages_test.py
messages_test.py:22: error: Too many arguments for "show_count"
Found 1 error in 1 file (checked 1 source file)
Então edito show_count
, acrescentando o argumento opcional plural
no Exemplo 3.
showcount
de hints_2/messages.py com um argumento opcionallink:code/08-def-type-hints/messages/hints_2/messages.py[role=include]
E agora o Mypy reporta "Success."
Warning
|
Aqui está um erro de digitação que o Python não reconhece. Você consegue encontrá-lo? def hex2rgb(color=str) -> tuple[int, int, int]: O relatório de erros do Mypy não é muito útil:
A dica de tipo para o argumento Pela minha experiência, esse é um erro comum e fácil de passar desapercebido, especialmente em dicas de tipo complexas. |
Os seguintes detalhes são considerados um bom estilo para dicas de tipo:
-
Sem espaço entre o nome do parâmetro e o
:
; um espaço após o:
-
Espaços dos dois lados do
=
que precede um valor default de parâmetro
Por outro lado, a PEP 8 diz que não deve haver espaço em torno de =
se não há nenhuma dica de tipo para aquele parâmetro específico.
Em vez de decorar essas regrinhas bobas, use ferramentas como flake8 e blue. O flake8 informa sobre o estilo do código e várias outras questões, enquanto o blue reescreve o código-fonte com base na (maioria) das regras prescritas pela ferramenta de formatação de código black.
Se o objetivo é impor um estilo de programação "padrão", blue é melhor que black, porque segue o estilo próprio do Python, de usar aspas simples por default e aspas duplas como alternativa.
>>> "I prefer single quotes"
'I prefer single quotes'
No CPython, a preferência por aspas simples está incorporada no repr()
, entre outros lugares. O módulo doctest depende do repr()
usar aspas simples por default.
Um dos autores do blue é Barry Warsaw, co-autor da PEP 8, core developer do Python desde 1994 e membro do Python’s Steering Council desde 2019. Daí estamos em ótima companhia quando escolhemos usar aspas simples.
Se você precisar mesmo usar o black, use a opção black -S
. Isso deixará suas aspas intocadas.
No Exemplo 3, o parâmetro plural
está anotado como str
, e o valor default é ''
. Assim não há conflito de tipo.
Eu gosto dessa solução, mas em outros contextos None
é um default melhor. Se o parâmetro opcional requer um tipo mutável, então None
é o único default sensato, como vimos na [mutable_default_parameter_sec].
Com None
como default para o parâmetro plural
, a assinatura ficaria assim:
from typing import Optional
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:
Vamos destrinchar essa linha:
-
Optional[str]
significa queplural
pode ser umastr
ouNone
. -
É obrigatório fornecer explicitamente o valor default
= None
.
Se você não atribuir um valor default a plural
, o runtime do Python vai tratar o parâmetro como obrigatório. Lembre-se: durante a execução do programa, as dicas de tipo são ignoradas.
Veja que é preciso importar Optional
do módulo typing
. Quando importamos tipos, é uma boa prática usar a sintaxe from typing import X
, para reduzir o tamanho das assinaturas das funções.
Warning
|
|
Agora que tivemos um primeiro contato concreto com a tipagem gradual, vamos examinar o que o conceito de tipo significa na prática.
Há muitas definições do conceito de tipo na literatura. Aqui vamos assumir que tipo é um conjunto de valores e um conjunto de funções que podem ser aplicadas àqueles valores.
Na prática, é mais útil considerar o conjunto de operações possíveis como a caraterística definidora de um tipo.[2]
Por exemplo, pensando nas operações possíveis, quais são os tipos válidos para x
na função a seguir?
def double(x):
return x * 2
O tipo do parâmetro x
pode ser numérico (int
, complex
, Fraction
, numpy.uint32
, etc.), mas também pode ser uma sequência (str
, tuple
, list
, array
), uma numpy.array
N-dimensional, ou qualquer outro tipo que implemente ou herde um método __mul__
que aceite um inteiro como argumento.
Entretanto, considere a anotação double
abaixo. Ignore por enquanto a ausência do tipo do retorno, vamos nos concentrar no tipo do parâmetro:
from collections import abc
def double(x: abc.Sequence):
return x * 2
Um verificador de tipo irá rejeitar esse código.
Se você informar ao Mypy que x
é do tipo abc.Sequence
, ele vai marcar x * 2
como erro, pois a Sequence
ABC não implementa ou herda o método __mul__
. Durante a execução, o código vai funcionar com sequências concretas como str
, tuple
, list
, array
, etc., bem como com números, pois durante a execução as dicas de tipo são ignoradas. Mas o verificador de tipo se preocupa apenas com o que estiver explicitamente declarado, e abc.Sequence
não suporta __mul__
.
Por essa razão o título dessa seção é "Tipos São Definidos pelas Operações Possíveis." O runtime do Python aceita qualquer objeto como argumento x
nas duas versões da função double
. O cálculo de x * 2
pode funcionar, ou pode causar um TypeError
, se a operação não for suportada por x
. Por outro lado, Mypy vai marcar x * 2
como um erro quando analisar o código-fonte anotado de double
, pois é uma operação não suportada pelo tipo declarado x: abc.Sequence
.
Em um sistema de tipagem gradual, acontece uma interação entre duas perspectivas diferentes de tipo:
- Duck typing ("tipagem pato")
-
A perspectiva adotada pelo Smalltalk — a primeira linguagem orientada a objetos — bem como em Python, JavaScript, e Ruby. Objetos tem tipo, mas variáveis (incluindo parâmetros) não. Na prática, não importa qual o tipo declarado de um objeto, importam apenas as operações que ele efetivamente suporta. Se eu posso invocar
birdie.quack()
então, nesse contexto,birdie
é um pato. Por definição, duck typing só é aplicada durante a execução, quando se tenta aplicar operações sobre os objetos. Isso é mais flexível que a tipagem nominal, ao preço de permitir mais erros durante a execução.[3] - Tipagem nominal
-
É a perspectiva adotada em C++, Java, e C#, e suportada em Python anotado. Objetos e variáveis tem tipos. Mas objetos só existem durante a execução, e o verificador de tipo só se importa com o código-fonte, onde as variáveis (incluindo parâmetros de função) tem anotações com dicas de tipo. Se
Duck
é uma subclasse deBird
, você pode atribuir uma instância deDuck
a um parâmetro anotado comobirdie: Bird
. Mas no corpo da função, o verificador considera a chamadabirdie.quack()
ilegal, poisbirdie
é nominalmente umBird
, e aquela classe não fornece o método.quack()
. Não interessa que o argumento real, durante a execução, é umDuck
, porque a tipagem nominal é aplicada de forma estática. O verificador de tipo não executa qualquer pedaço do programa, ele apenas lê o código-fonte. Isso é mais rígido que duck typing, com a vantagem de capturar alguns bugs durante o desenvolvimento, ou mesmo em tempo real, enquanto o código está sendo digitado em um IDE.
O Exemplo 4 é um exemplo bobo que contrapõe duck typing e tipagem nominal, bem como verificação de tipo estática e comportamento durante a execução.[4]
link:code/08-def-type-hints/birds/birds.py[role=include]
-
Duck
é uma subclasse deBird
. -
alert
não tem dicas de tipo, então o verificador a ignora. -
alert_duck
aceita um argumento do tipoDuck
. -
alert_bird
aceita um argumento do tipoBird
.
Verificando birds.py com Mypy, encontramos um problema:
…/birds/ $ mypy birds.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)
Só de analisar o código fonte, Mypy percebe que alert_bird
é problemático: a dica de tipo declara o parâmetro birdie
como do tipo Bird
, mas o corpo da função chama birdie.quack()
— e a classe Bird
não tem esse método.
Agora vamos tentar usar o módulo birds
em daffy.py no Exemplo 5.
link:code/08-def-type-hints/birds/daffy.py[role=include]
-
Chamada válida, pois
alert
não tem dicas de tipo. -
Chamada válida, pois
alert_duck
recebe um argumento do tipoDuck
edaffy
é umDuck
. -
Chamada válida, pois
alert_bird
recebe um argumento do tipoBird
, edaffy
também é umBird
— a superclasse deDuck
.
Mypy reporta o mesmo erro em daffy.py, sobre a chamada a quack
na função alert_bird
definida em birds.py:
…/birds/ $ mypy daffy.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)
Mas o Mypy não vê qualquer problema com daffy.py em si: as três chamadas de função estão OK.
Agora, rodando daffy.py, o resultado é o seguinte:
…/birds/ $ python3 daffy.py
Quack!
Quack!
Quack!
Funciona perfeitamente! Viva o duck typing!
Durante a execução do programa, o Python não se importa com os tipos declarados. Ele usa apenas duck typing. O Mypy apontou um erro em alert_bird
, mas a chamada da função com daffy
funciona corretamente quando executada.
À primeira vista isso pode surpreender muitos pythonistas: um verificador de tipo estático muitas vezes encontra erros em código que sabemos que vai funcionar quanto executado.
Entretanto, se daqui a alguns meses você for encarregado de estender o exemplo bobo do pássaro, você agradecerá ao Mypy. Observe esse módulo woody.py module, que também usa birds
, no Exemplo 6.
link:code/08-def-type-hints/birds/woody.py[role=include]
O Mypy encontra dois erros ao verificar woody.py:
…/birds/ $ mypy woody.py
birds.py:16: error: "Bird" has no attribute "quack"
woody.py:5: error: Argument 1 to "alert_duck" has incompatible type "Bird";
expected "Duck"
Found 2 errors in 2 files (checked 1 source file)
O primeiro erro é em birds.py: a chamada a birdie.quack()
em alert_bird
, que já vimos antes.
O segundo erro é em woody.py: woody
é uma instância de Bird
, então a chamada alert_duck(woody)
é inválida, pois aquela função exige um Duck.
Todo Duck
é um Bird
, mas nem todo Bird
é um Duck
.
Durante a execução, nenhuma das duas chamadas em woody.py funcionariam. A sucessão de falhas é melhor ilustrada em uma sessão no console, através das mensagens de erro, no Exemplo 7.
>>> from birds import *
>>> woody = Bird()
>>> alert(woody) # (1)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_duck(woody) # (2)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
>>>
>>> alert_bird(woody) # (3)
Traceback (most recent call last):
...
AttributeError: 'Bird' object has no attribute 'quack'
-
O Mypy não tinha como detectar esse erro, pois não há dicas de tipo em
alert
. -
O Mypy avisou do problema:
Argument 1 to "alert_duck" has incompatible type "Bird"; expected "Duck"
(Argumento 1 paraalert_duck
é do tipo incompatível "Bird"; argumento esperado era "Duck") -
O Mypy está avisando desde o Exemplo 4 que o corpo da função
alert_bird
está errado:"Bird" has no attribute "quack"
(Bird não tem um atributo "quack")
Este pequeno experimento mostra que o duck typing é mais fácil para o iniciante e mais flexível, mas permite que operações não suportadas causem erros durante a execução.
A tipagem nominal detecta os erros antes da execução, mas algumas vezes rejeita código que seria executado sem erros - como a chamada a alert_bird(daffy)
no Exemplo 5.
Mesmo que funcione algumas vezes, o nome da função alert_bird
está incorreto: seu código exige um objeto que suporte o método .quack()
, que não existe em Bird
.
Nesse exemplo bobo, as funções tem uma linha apenas.
Mas na vida real elas poderiam ser mais longas, e poderiam passar o argumento birdie
para outras funções, e a origem daquele argumento poderia estar a muitas chamadas de função de distância, tornando difícil localizar a causa do erro durante a execução.
O verificador de tipos impede que muitos erros como esse aconteçam durante a execução de um programa.
Note
|
O valor das dicas de tipo é questionável em exemplos minúsculo que cabem em um livro. Os benefícios crescem conforme o tamanho da base de código afetada. É por essa razão que empresas com milhões de linhas de código em Python - como a Dropbox, o Google e o Facebook - investiram em equipes e ferramentas para promover a adoção global de dicas de tipo internamente, e hoje tem partes significativas e crescentes de sua base de código checadas para tipo em suas linhas (pipeline) de integração contínua. |
Nessa seção exploramos as relações de tipos e operações no duck typing e na tipagem nominal, começando com a função simples double()
— que deixamos sem dicas de tipo. Agora vamos dar uma olhada nos tipos mais importantes ao anotar funções.
Vamos ver um bom modo de adicionar dicas de tipo a double()
quando examinarmos Protocolos estáticos. Mas antes disso, há tipos mais importantes para conhecer.
Quase todos os tipos em Python podem ser usados em dicas de tipo, mas há restrições e recomendações. Além disso, o módulo typing
introduziu constructos especiais com uma semântica às vezes surpreendente.
Essa seção trata de todos os principais tipos que você pode usar em anotações:
-
typing.Any
-
Tipos e classes simples
-
typing.Optional
etyping.Union
-
Coleções genéricas, incluindo tuplas e mapeamentos
-
Classes base abstratas
-
Iteradores genéricos
-
Genéricos parametrizados e
TypeVar
-
typing.Protocols
— crucial para duck typing estático -
typing.Callable
-
typing.NoReturn
— um bom modo de encerrar essa lista.
Vamos falar de um de cada vez, começando por um tipo que é estranho, aparentemente inútil, mas de uma importância fundamental.
A pedra fundamental de qualquer sistema gradual de tipagem é o tipo Any
, também conhecido como o tipo dinâmico. Quando um verificador de tipo vê um função sem tipo como esta:
def double(x):
return x * 2
ele supõe isto:
def double(x: Any) -> Any:
return x * 2
Isso significa que o argumento x
e o valor de retorno podem ser de qualquer tipo, inclusive de tipos diferentes. Assume-se que Any
pode suportar qualquer operação possível.
Compare Any
com object
. Considere essa assinatura:
def double(x: object) -> object:
Essa função também aceita argumentos de todos os tipos, porque todos os tipos são subtipo-de object
.
Entretanto, um verificador de tipo vai rejeitar essa função:
def double(x: object) -> object:
return x * 2
O problema é que object
não suporta a operação __mul__
. Veja o que diz o Mypy:
…/birds/ $ mypy double_object.py
double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)
Tipos mais gerais tem interfaces mais restritas, isto é, eles suportam menos operações. A classe object
implementa menos operações que abc.Sequence
,
que implementa menos operações que abc.MutableSequence
,
que por sua vez implementa menos operações que list
.
Mas Any
é um tipo mágico que reside tanto no topo quanto na base da hierarquia de tipos. Ele é simultaneamente o tipo mais geral - então um argumento n: Any
aceita valores de qualquer tipo - e o tipo mais especializado, suportando assim todas as operações possíveis.
Pelo menos é assim que o verificador de tipo entende Any
.
Claro, nenhum tipo consegue suportar qualquer operação possível, então usar Any
impede o verificador de tipo de cumprir sua missão primária: detectar operações potencialmente ilegais antes que seu programa falhe e levante uma exceção durante sua execução.
Sistemas tradicionais de tipagem nominal orientados a objetos se baseiam na relação subtipo-de.
Dada uma classe T1
e uma subclasse T2
, então T2
é subtipo-de T1
.
Observe este código:
class T1:
...
class T2(T1):
...
def f1(p: T1) -> None:
...
o2 = T2()
f1(o2) # OK
A chamada f1(o2)
é uma aplicação do Princípio de Substituição de Liskov (Liskov Substitution Principle—LSP).
Barbara Liskov[5] na verdade definiu é subtipo-de em termos das operações suportadas. Se um objeto do tipo T2
substitui um objeto do tipo T1
e o programa continua se comportando de forma correta, então T2
é subtipo-de T1
.
Seguindo com o código visto acima, essa parte mostra uma violação do LSP:
def f2(p: T2) -> None:
...
o1 = T1()
f2(o1) # type error
Do ponto de vista das operações suportadas, faz todo sentido: como uma subclasse, T2
herda e precisa suportar todas as operações suportadas por T1
. Então uma instância de T2
pode ser usada em qualquer lugar onde se espera uma instância de T1
. Mas o contrário não é necessariamente verdadeiro: T2
pode implementar métodos adicionais, então uma instância de T1
não pode ser usada onde se espera uma instância de T2
. Este foco nas operações suportadas se reflete no nome _behavioral subtyping (subtipagem comportamental) (EN), também usado para se referir ao LSP.
Em um sistema de tipagem gradual há outra relação, consistente-com (consistent-with), que se aplica sempre que subtipo-de puder ser aplicado, com disposições especiais para o tipo Any
.
As regras para consistente-com são:
-
Dados
T1
e um subtipoT2
, entãoT2
é consistente-comT1
(substituição de Liskov). -
Todo tipo é consistente-com
Any
: você pode passar objetos de qualquer tipo em um argumento declarado como de tipo `Any. -
Any
é consistente-com todos os tipos: você sempre pode passar um objeto de tipoAny
onde um argumento de outro tipo for esperado.
Considerando as definições anteriores dos objetos o1
e o2
, aqui estão alguns exemplos de código válido, ilustrando as regras #2 e #3:
def f3(p: Any) -> None:
...
o0 = object()
o1 = T1()
o2 = T2()
f3(o0) #
f3(o1) # tudo certo: regra #2
f3(o2) #
def f4(): # tipo implícito de retorno: `Any`
...
o4 = f4() # tipo inferido: `Any`
f1(o4) #
f2(o4) # tudo certo: regra #3
f3(o4) #
Todo sistema de tipagem gradual precisa de um tipo coringa como Any
Tip
|
O verbo "inferir" é um sinônimo bonito para "adivinhar", quando usado no contexto da análise de tipos. Verificadores de tipo modernos, em Python e outras linguagens, não precisam de anotações de tipo em todo lugar porque conseguem inferir o tipo de muitas expressões. Por exemplo, se eu escrever |
Agora podemos explorar o restante dos tipos usados em anotações.
Tipos simples como int
, float
, str
, e bytes
podem ser usados diretamente em dicas de tipo.
Classes concretas da biblioteca padrão, de pacotes externos ou definidas pelo usuário — FrenchDeck
, Vector2d
, e Duck
- também podem ser usadas em dicas de tipo.
Classes base abstratas também são úteis aqui. Voltaremos a elas quando formos estudar os tipos coleção, e em Classes bases abstratas.
Para classes, consistente-com é definido como subtipo_de: uma subclasse é consistente-com todas as suas superclasses.
Entretanto, "a praticidade se sobrepõe à pureza", então há uma exceção importante, discutida em seguida.
Tip
|
int é Consistente-Com complex
Não há nenhuma relação nominal de subtipo entre os tipo nativos |
Nós vimos o tipo especial Optional
em Usando None como default. Ele resolve o problema de ter None
como default, como no exemplo daquela seção:
from typing import Optional
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:
A sintaxe Optional[str]
é na verdade um atalho para Union[str, None]
,
que significa que o tipo de plural
pode ser str
ou None
.
Tip
|
Uma sintaxe melhor para Optional e Union em Python 3.10
Desde o Python 3.10 é possível escrever plural: Optional[str] = None # before
plural: str | None = None # after O operador |
A assinatura da função nativa ord
é um exemplo simples de Union
- ela aceita str
or bytes
,
e retorna um int
:[6]
def ord(c: Union[str, bytes]) -> int: ...
Aqui está um exemplo de uma função que aceita uma str
, mas pode retornar uma str
ou um float
:
from typing import Union
def parse_token(token: str) -> Union[str, float]:
try:
return float(token)
except ValueError:
return token
Se possível, evite criar funções que retornem o tipo Union
, pois esse tipo exige um esforço extra do usuário: pois para saber o que fazer com o valor recebido da função será necessário verificar o tipo daquele valor durante a execução.
Mas a parse_token
no código acima é um caso de uso razoável no contexto de interpretador de expressões simples.
Tip
|
Na [dual_mode_api_sec], vimos funções que aceitam tanto |
Union[]
exige pelo menos dois tipos.
Tipos Union
aninhados tem o mesmo efeito que uma Union
"achatada" .
Então esta dica de tipo:
Union[A, B, Union[C, D, E]]
é o mesmo que:
Union[A, B, C, D, E]
Union
é mais útil com tipos que não sejam consistentes entre si. Por exemplo: Union[int, float]
é redundante, pois int
é consistente-com float
. Se você usar apenas float
para anotar o parâmetro, ele vai também aceitar valores int
.
A maioria das coleções em Python são heterogêneas.
Por exemplo, você pode inserir qualquer combinação de tipos diferentes em uma list
.
Entretanto, na prática isso não é muito útil: se você colocar objetos em uma coleção, você certamente vai querer executar alguma operação com eles mais tarde, e normalmente isso significa que eles precisam compartilhar pelo menos um método comum.[7]
Tipos genéricos podem ser declarados com parâmetros de tipo, para especificar o tipo de item com o qual eles conseguem trabalhar.
Por exemplo, uma list
pode ser parametrizada para restringir o tipo de elemento ali contido, como se pode ver no Exemplo 8.
tokenize
com dicas de tipo para Python ≥ 3.9def tokenize(text: str) -> list[str]:
return text.upper().split()
Em Python ≥ 3.9, isso significa que tokenize
retorna uma list
onde todos os elementos são do tipo str
.
As anotações stuff: list
e stuff: list[Any]
significam a mesma coisa: stuff
é uma lista de objetos de qualquer tipo.
Tip
|
Se você estiver usando Python 3.8 ou anterior, o conceito é o mesmo, mas você precisa de mais código para funcionar - como explicado em Suporte a tipos de coleção descontinuados. |
A PEP 585—Type Hinting Generics In Standard Collections (EN) lista as coleções da biblioteca padrão que aceitam dicas de tipo genéricas. A lista a seguir mostra apenas as coleções que usam a forma mais simples de dica de tipo genérica, container[item]
:
list collections.deque abc.Sequence abc.MutableSequence
set abc.Container abc.Set abc.MutableSet
frozenset abc.Collection
Os tipos tuple
e mapping aceitam dicas de tipo mais complexas, como veremos em suas respectivas seções.
No Python 3.10, não há uma boa maneira de anotar array.array
, levando em consideração o argumento typecode
do construtor, que determina se o array contém inteiros ou floats. Um problema ainda mais complicado é verificar a faixa dos inteiros, para prevenir OverflowError
durante a execução, ao se adicionar novos elementos. Por exemplo, um array
com typecode=B
só pode receber valores int
de 0 a 255. Até o Python 3.11, o sistema de tipagem estática do Python não consegue lidar com esse desafio.
(Você pode pular esse box se usa apenas Python 3.9 ou posterior.)
Em Python 3.7 e 3.8, você precisa importar um __future__
para fazer a notação []
funcionar com as coleções nativas, tal como list
, como ilustrado no Exemplo 9.
tokenize
com dicas de tipo para Python ≥ 3.7from __future__ import annotations
def tokenize(text: str) -> list[str]:
return text.upper().split()
O __future__
não funciona com Python 3.6 ou anterior. O
Exemplo 10 mostra como anotar tokenize
de uma forma que funciona com Python ≥ 3.5.
tokenize
com dicas de tipo para Python ≥ 3.5from typing import List
def tokenize(text: str) -> List[str]:
return text.upper().split()
Para fornecer um suporte inicial a dicas de tipo genéricas,
os autores da PEP 484 criaram dúzias de tipos genéricos no módulo typing
.
A Tabela 1 mostra alguns deles.
Para a lista completa, consulte a documentação do módulo typing .
Collection | Type hint equivalent |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A PEP 585—Type Hinting Generics In Standard Collections deu início a um processo de vários anos para melhorar a usabilidade das dicas de tipo genéricas. Podemos resumir esse processo em quatro etapas:
-
Introduzir
from future import annotations
no Python 3.7 para permitir o uso das classes da biblioteca padrão como genéricos com a notaçãolist[str]
. -
Tornar aquele comportamento o default a partir do Python 3.9:
list[str]
agora funciona sem quefuture
precise ser importado. -
Descontinuar (deprecate) todos os tipos genéricos do módulo
typing
.[8] Avisos de descontinuação não serão emitidos pelo interpretador Python, porque os verificadores de tipo devem sinalizar os tipos descontinuados quando o programa sendo verificado tiver como alvo Python 3.9 ou posterior. -
Remover aqueles tipos genéricos redundantes na primeira versão de Python lançada cinco anos após o Python 3.9. No ritmo atual, esse deverá ser o Python 3.14, também conhecido como Python Pi.
Agora vamos ver como anotar tuplas genéricas.
Há três maneiras de anotar os tipos tuple
.
-
Tuplas como registros (records)
-
Tuplas como registro com campos nomeados
-
Tuplas como sequências imutáveis.
Se você está usando uma tuple
como um registro, use o tipo tuple
nativo e declare os tipos dos campos dentro dos []
.
Por exemplo, a dica de tipo seria tuple[str, float, str]
para aceitar uma tupla com nome da cidade, população e país:
('Shanghai', 24.28, 'China')
.
Observe uma função que recebe um par de coordenadas geográficas e retorna uma Geohash, usada assim:
>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'
O Exemplo 11 mostra a definição da função geohash
, usando o pacote geolib
do PyPI.
geohash
link:code/08-def-type-hints/coordinates/coordinates.py[role=include]
-
Esse comentário evita que o Mypy avise que o pacote
geolib
não tem nenhuma dica de tipo. -
O parâmetro
lat_lon
, anotado como umatuple
com dois camposfloat
.
Tip
|
Com Python < 3.9, importe e use |
Para a anotar uma tupla com muitos campos, ou tipos específicos de tupla que seu código usa com frequência, recomendo fortemente usar typing.NamedTuple
, como visto no [data_class_ch].
O Exemplo 12 mostra uma variante de Exemplo 11 com NamedTuple
.
NamedTuple
, Coordinates
e a função geohash
link:code/08-def-type-hints/coordinates/coordinates_named.py[role=include]
Como explicado na [data_class_overview_sec], typing.NamedTuple
é uma factory de subclasses de tuple
, então Coordinate
é consistente-com tuple[float, float]
, mas o inverso não é verdadeiro - afinal, Coordinate
tem métodos extras adicionados por NamedTuple
, como ._asdict()
, e também poderia ter métodos definidos pelo usuário.
Na prática, isso significa que é seguro (do ponto de vista do tipo de argumento) passar uma instância de Coordinate
para a função display
, definida assim:
link:code/08-def-type-hints/coordinates/coordinates_named.py[role=include]
Para anotar tuplas de tamanho desconhecido, usadas como listas imutáveis, você precisa especificar um único tipo, seguido de uma vírgula e …
(isto é o símbolo de reticências do Python, formado por três pontos, não o caractere Unicode U+2026
—HORIZONTAL ELLIPSIS
).
Por exemplo, tuple[int, …]
é uma tupla com itens int
.
As reticências indicam que qualquer número de elementos >= 1 é aceitável. Não há como especificar campos de tipos diferentes para tuplas de tamanho arbitrário.
As anotações stuff: tuple[Any, …]
e stuff: tuple
são equivalentes:
stuff
é uma tupla de tamanho desconhecido contendo objetos de qualquer tipo.
Aqui temos um função columnize
, que transforma uma sequência em uma tabela de colunas e células, na forma de uma lista de tuplas de tamanho desconhecido.
É útil para mostrar os itens em colunas, assim:
>>> animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus'.split()
>>> table = columnize(animals)
>>> table
[('drake', 'koala', 'yak'), ('fawn', 'lynx', 'zapus'), ('heron', 'tahr'),
('ibex', 'xerus')]
>>> for row in table:
... print(''.join(f'{word:10}' for word in row))
...
drake koala yak
fawn lynx zapus
heron tahr
ibex xerus
O Exemplo 13 mostra a implementação de columnize
.
Observe o tipo do retorno:
list[tuple[str, ...]]
link:code/08-def-type-hints/columnize.py[role=include]
Tipos de mapeamento genéricos são anotados como MappingType[KeyType, ValueType]
.
O tipo nativo dict
e os tipos de mapeamento em collections
e collections.abc
aceitam essa notação em Python ≥ 3.9. Para versões mais antigas, você deve usar typing.Dict
e outros tipos de mapeamento no módulo typing
, como discutimos em Suporte a tipos de coleção descontinuados.
O Exemplo 14 mostra um uso na prática de uma função que retorna um índice invertido para permitir a busca de caracteres Unicode pelo nome — uma variação do [ex_cfpy] mais adequada para código server-side (também chamado back-end), como veremos no [async_ch].
Dado o início e o final dos códigos de caractere Unicode, name_index
retorna um dict[str, set[str]]
, que é um índice invertido mapeando cada palavra para um conjunto de caracteres que tem aquela palavra em seus nomes. Por exemplo, após indexar os caracteres ASCII de 32 a 64, aqui estão os conjuntos de caracteres mapeados para as palavras 'SIGN'
e 'DIGIT'
, e a forma de encontrar o caractere chamado 'DIGIT EIGHT'
:
>>> index = name_index(32, 65)
>>> index['SIGN']
{'$', '>', '=', '+', '<', '%', '#'}
>>> index['DIGIT']
{'8', '5', '6', '2', '3', '0', '1', '4', '7', '9'}
>>> index['DIGIT'] & index['EIGHT']
{'8'}
O Exemplo 14 mostra o código fonte de charindex.py com a função name_index
.
Além de uma dica de tipo dict[]
, este exemplo tem três outros aspectos que estão aparecendo pela primeira vez no livro.
link:code/08-def-type-hints/charindex.py[role=include]
-
tokenize
é uma função geradora. [iterables2generators] é sobre geradores. -
A variável local
index
está anotada. Sem a dica, o Mypy diz:Need type annotation for 'index' (hint: "index: dict[<type>, <type>] = …")
. -
Eu usei o operador morsa (walrus operator)
:=
na condição doif
. Ele atribui o resultado da chamada aunicodedata.name()
aname
, e a expressão inteira é calculada a partir daquele resultado. Quando o resultado é''
, isso é falso, e oindex
não é atualizado.[9]
Note
|
Ao usar |
Seja conservador no que envia, mas liberal no que aceita.
ou o Princípio da Robustez
A Tabela 1 apresenta várias classes abstratas de collections.abc
.
Idealmente, uma função deveria aceitar argumentos desses tipos abstratos—ou seus equivalentes de typing
antes do Python 3.9—e não tipos concretos. Isso dá mais flexibilidade a quem chama a função.
Considere essa assinatura de função:
from collections.abc import Mapping
def name2hex(name: str, color_map: Mapping[str, int]) -> str:
Usar abc.Mapping
permite ao usuário da função fornecer uma instância de dict
, defaultdict
, ChainMap
, uma subclasse de UserDict
subclass, ou qualquer outra classe que seja um subtipo-de Mapping
.
Por outro lado, veja essa assinatura:
def name2hex(name: str, color_map: dict[str, int]) -> str:
Agora color_map
tem que ser um dict
ou um de seus subtipos, tal como defaultdict
ou OrderedDict
.
Especificamente, uma subclasse de collections.UserDict
não passaria pela verificação de tipo para color_map
, a despeito de ser a maneira recomendada de criar mapeamentos definidos pelo usuário, como vimos na [sublcassing_userdict_sec].
O Mypy rejeitaria um UserDict
ou uma instância de classe derivada dele, porque UserDict
não é uma subclasse de dict
; eles são irmãos. Ambos são subclasses de abc.MutableMapping
.[10]
Assim, em geral é melhor usar abc.Mapping
ou abc.MutableMapping
em dicas de tipos de parâmetros, em vez de dict
(ou typing.Dict
em código antigo).
Se a função name2hex
não precisar modificar o color_map
recebido, a dica de tipo mais precisa para color_map
é abc.Mapping
.
Desse jeito, quem chama não precisa fornecer um objeto que implemente métodos como setdefault
, pop
, e update
, que fazem parte da interface de MutableMapping
, mas não de Mapping
.
Isso reflete a segunda parte da lei de Postel:
"[seja] liberal no que aceita."
A lei de Postel também nos diz para sermos conservadores no que enviamos. O valor de retorno de uma função é sempre um objeto concreto, então a dica de tipo do valor de saída deve ser um tipo concreto, como no exemplo em Coleções genéricas — que usa list[str]
:
def tokenize(text: str) -> list[str]:
return text.upper().split()
No verbete de typing.List
(EN - Tradução abaixo não oficial), a documentação do Python diz:
Versão genérica de
list
. Útil para anotar tipos de retorno. Para anotar argumentos é preferível usar um tipo de coleção abstrata , tal comoSequence
ouIterable
.
Comentários similares aparecem nos verbetes de typing.Dict
e typing.Set
.
Lembre-se que a maioria dos ABCs de collections.abc
e outras classes concretas de collections
, bem como as coleções nativas, suportam notação de dica de tipo genérica como collections.deque[str]
desde o Python 3.9. As coleções correspondentes em typing
só precisavam suportar código escrito em Python 3.8 ou anterior. A lista completa de classes que se tornaram genéricas aparece em na seção "Implementation" da
PEP 585—Type Hinting Generics In Standard Collections (EN).
Para encerrar nossa discussão de ABCs em dicas de tipo, precisamos falar sobre os ABCs numbers
.
O pacote numbers
define a assim chamada torre numérica (numeric tower) descrita na PEP 3141—A Type Hierarchy for Numbers (EN).
A torre é uma hierarquia linear de ABCs, com Number
no topo:
-
Number
-
Complex
-
Real
-
Rational
-
Integral
Esses ABCs funcionam perfeitamente para checagem de tipo durante a execução, mas eles não são suportados para checagem de tipo estática. A seção "Numeric Tower" da PEP 484 rejeita os ABCs numbers
e manda tratar os tipo nativos complex
, float
, e int
como casos especiais, como explicado em int é Consistente-Com complex. Vamos voltar a essa questão na [numbers_abc_proto_sec], em [ifaces_prot_abc], que é dedicada a comparar protocolos e ABCs
Na prática, se você quiser anotar argumentos numéricos para checagem de tipo estática, existem algumas opções:
-
Usar um dos tipo concretos,
int
,float
, oucomplex
— como recomendado pela PEP 488. -
Declarar um tipo union como
Union[float, Decimal, Fraction]
. -
Se você quiser evitar a codificação explícita de tipos concretos, usar protocolos numéricos como
SupportsFloat
, tratados na [runtime_checkable_proto_sec].
A Protocolos estáticos abaixo é um pré-requisito para entender protocolos numéricos.
Antes disso, vamos examinar um dos ABCs mais úteis para dicas de tipo: Iterable
.
A documentação de typing.List
que eu citei acima recomenda Sequence
e Iterable
para dicas de tipo de parâmetros de função.
Esse é um exemplo de argumento Iterable
, na função math.fsum
da biblioteca padrão:
def fsum(__seq: Iterable[float]) -> float:
Tip
|
Arquivos Stub e o Projeto Typeshed
Até o Python 3.10, a biblioteca padrão não tem anotações, mas o Mypy, o PyCharm, etc, conseguem encontrar as dicas de tipo necessárias no projeto Typeshed, na forma de arquivos stub: arquivos de código-fonte especiais, com uma extensão .pyi, que contém assinaturas anotadas de métodos e funções, sem a implementação - muito parecidos com headers em C. A assinatura para |
O Exemplo 15 é outro exemplo do uso de um parâmetro Iterable
, que produz itens que são tuple[str, str]
. A função é usada assim:
>>> l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
>>> text = 'mad skilled noob powned leet'
>>> from replacer import zip_replace
>>> zip_replace(text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'
O Exemplo 15 mostra a implementação.
link:code/08-def-type-hints/replacer.py[role=include]
-
FromTo
é um apelido de tipo: eu atribuituple[str, str]
aFromTo
, para tornar a assinatura dezip_replace
mais legível. -
changes
tem que ser umIterable[FromTo]
; é o mesmo que escreverIterable[tuple[str, str]]
, mas é mais curto e mais fácil de ler.
Tip
|
O TypeAlias Explícito em Python 3.10
PEP 613—Explicit Type Aliases introduziu um tipo especial, o from typing import TypeAlias
FromTo: TypeAlias = tuple[str, str] |
Tanto math.fsum
quanto replacer.zip_replace
tem que percorrer todos os argumentos do Iterable
para produzir um resultado. Dado um iterável sem fim tal como o gerador itertools.cycle
como entrada, essas funções consumiriam toda a memória e derrubariam o processo Python. Apesar desse perigo potencial, é muito comum no Python moderno se oferecer funções que aceitam um Iterable
como argumento, mesmo se elas tem que processar a estrutura inteira para obter um resultado. Isso dá a quem chama a função a opção de fornecer um gerador como dado de entrada, em vez de uma sequência pré-construída, com uma grande economia potencial de memória se o número de itens de entrada for grande.
Por outro lado, a função columnize
no Exemplo 13 requer uma Sequence
, não um Iterable
, pois ela precisa obter a len()
do argumento para calcular previamente o número de linhas.
Assim como Sequence
, o melhor uso de Iterable
é como tipo de argumento. Ele é muito vago como um tipo de saída. Uma função deve ser mais precisa sobre o tipo concreto que retorna.
O tipo Iterator
, usado como tipo do retorno no Exemplo 14, está intimamente relacionado a Iterable
. Voltaremos a ele em [iterables2generators], que trata de geradores e iteradores clássicos.
Um genérico parametrizado é um tipo genérico, escrito na forma list[T]
, onde T
é um tipo variável que será vinculado a um tipo específico a cada uso. Isso permite que um tipo de parâmetro seja refletido no tipo resultante.
O Exemplo 16 define sample
, uma função que recebe dois argumentos:
uma Sequence
de elementos de tipo T
e um int
.
Ela retorna uma list
de elementos do mesmo tipo T
, escolhidos aleatoriamente do primeiro argumento.
O Exemplo 16 mostra a implementação.
link:code/08-def-type-hints/sample.py[role=include]
Aqui estão dois exemplos do motivo de eu usar um tipo variável em sample
:
-
Se chamada com uma tupla de tipo
tuple[int, …]
— que é consistente-comSequence[int]
- então o tipo parametrizado éint
, então o tipo de retorno élist[int]
. -
Se chamada com uma
str
— que é consistente-comSequence[str]
— então o tipo parametrizado éstr
, e o tipo do retorno élist[str]
.
Note
|
Por que TypeVar é necessário?
Os autores da PEP 484 queriam introduzir dicas de tipo ao acrescentar o módulo |
Outro exemplo é a função statistics.mode
da biblioteca padrão, que retorna o ponto de dado mais comum de uma série.
Aqui é uma exemplo de uso da documentação:
>>> mode([1, 1, 2, 3, 3, 3, 3, 4])
3
Sem o uso de TypeVar
, mode
poderia ter uma assinatura como a apresentada no Exemplo 17.
link:code/08-def-type-hints/mode/mode_float.py[role=include]
Muitos dos usos de mode
envolvem valores int
ou float
, mas o Python tem outros tipos numéricos, e é desejável que o tipo de retorno siga o tipo dos elementos do Iterable
recebido.
Podemos melhorar aquela assinatura usando TypeVar
. Vamos começar com uma assinatura parametrizada simples, mas errada.
from collections.abc import Iterable
from typing import TypeVar
T = TypeVar('T')
def mode(data: Iterable[T]) -> T:
Quando aparece pela primeira vez na assinatura, o tipo parametrizado T
pode ser qualquer tipo. Da segunda vez que aparece, ele vai significar o mesmo tipo que da primeira vez.
Assim, qualquer iterável é consistente-com Iterable[T]
, incluindo iterável de tipos unhashable que collections.Counter
não consegue tratar.
Precisamos restringir os tipos possíveis de se atribuir a T
.
Vamos ver maneiras diferentes de fazer isso nas duas seções seguintes.
O TypeVar
aceita argumentos posicionais adicionais para restringir o tipo parametrizado.
Podemos melhorar a assinatura de mode
para aceitar um número específico de tipos, assim:
from collections.abc import Iterable
from decimal import Decimal
from fractions import Fraction
from typing import TypeVar
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
def mode(data: Iterable[NumberT]) -> NumberT:
Está melhor que antes, e era a assinatura de mode
em
statistics.pyi, o arquivo stub em typeshed
em 25 de maio de 2020.
Entretanto, a documentação em statistics.mode
inclui esse exemplo:
>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'
Na pressa, poderíamos apenas adicionar str
à definição de NumberT
:
NumberT = TypeVar('NumberT', float, Decimal, Fraction, str)
Com certeza funciona, mas NumberT
estaria muito mal batizado se aceitasse str
.
Mais importante, não podemos ficar listando tipos para sempre, cada vez que percebermos que mode
pode lidar com outro deles.
Podemos fazer com melhor com um outro recurso de TypeVar
, como veremos a seguir.
Examinando o corpo de mode
no Exemplo 17, vemos que a classe Counter
é usada para classificação. Counter
é baseada em dict
, então o tipo do elemento do iterável data
precisa ser hashable.
A princípio, essa assinatura pode parecer que funciona:
from collections.abc import Iterable, Hashable
def mode(data: Iterable[Hashable]) -> Hashable:
Agora o problema é que o tipo do item retornado é Hashable
:
um ABC que implementa apenas o método __hash__
.
Então o verificador de tipo não vai permitir que façamos nada com o valor retornado, exceto chamar seu método hash()
. Não é muito útil.
A solução está em outro parâmetro opcional de TypeVar
: o parâmetro representado pela palavra-chave bound
. Ele estabelece um limite superior para os tipos aceitos.
No Exemplo 18, temos bound=Hashable
. Isso significa que o tipo do parâmetro pode ser Hashable
ou qualquer subtipo-de Hashable
.[12]
link:code/08-def-type-hints/mode/mode_hashable.py[role=include]
Em resumo:
-
Um tipo variável restrito será concretizado em um dos tipos nomeados na declaração do TypeVar.
-
Um tipo variável delimitado será concretizado pata o tipo inferido da expressão - desde que o tipo inferido seja consistente-com o limite declarado pelo argumento
bound=
do TypeVar.
Note
|
É um pouco lamentável que a palavra-chave do argumento para declarar um |
O construtor de typing.TypeVar
tem outros parâmetros opcionais - covariant
e contravariant
— que veremos em [more_types_ch], [variance_sec].
Agora vamos concluir essa introdução a TypeVar
com AnyStr
.
O módulo typing
inclui um TypeVar pré-definido chamado AnyStr
.
Ele está definido assim:
AnyStr = TypeVar('AnyStr', bytes, str)
AnyStr
é usado em muitas funções que aceitam tanto bytes
quanto str
, e retornam valores do tipo recebido.
Agora vamos ver typing.Protocol
, um novo recurso do Python 3.8, capaz de permitir um uso de dicas de tipo mais pythônico.
Note
|
Em programação orientada a objetos, o conceito de um "protocolo" como uma interface informal é tão antigo quando o Smalltalk, e foi uma parte essencial do Python desde o início. Entretanto, no contexto de dicas de tipo, um protocolo é uma subclasse de |
O tipo Protocol
, como descrito em
PEP 544—Protocols: Structural subtyping (static duck typing) (EN), é similar às interfaces em Go: um tipo protocolo é definido especificando um ou mais métodos, e o verificador de tipo analisa se aqueles métodos estão implementados onde um tipo daquele protocolo é usado.
Em Python, uma definição de protocolo é escrita como uma subclasse de typing.Protocol
.
Entretanto, classes que implementam um protocolo não precisam herdar, registrar ou declarar qualquer relação com a classe que define o protocolo. É função do verificador de tipo encontrar os tipos de protocolos disponíveis e exigir sua utilização.
Abaixo temos um problema que pode ser resolvido com a ajuda de Protocol
e TypeVar
.
Suponha que você quisesse criar uma função top(it, n)
, que retorna os n
maiores elementos do iterável it
:
link:code/08-def-type-hints/comparable/top.py[role=include]
Um genérico parametrizado top
ficaria parecido com o mostrado no Exemplo 19.
top
function com um parâmetro de tipo T
indefinidodef top(series: Iterable[T], length: int) -> list[T]:
ordered = sorted(series, reverse=True)
return ordered[:length]
O problema é, como restringir T
?
Ele não pode ser Any
ou object
, pois series
precisa funcionar com sorted
. A sorted
nativa na verdade aceita Iterable[Any]
, mas só porque o parâmetro opcional key
recebe uma função que calcula uma chave de ordenação arbitrária para cada elemento. O que acontece se você passar para sorted
uma lista de objetos simples, mas não fornecer um argumento key
?
Vamos tentar:
>>> l = [object() for _ in range(4)]
>>> l
[<object object at 0x10fc2fca0>, <object object at 0x10fc2fbb0>,
<object object at 0x10fc2fbc0>, <object object at 0x10fc2fbd0>]
>>> sorted(l)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'object' and 'object'
A mensagem de erro mostra que sorted
usa o operador <
nos elementos do iterável.
É só isso? Vamos tentar outro experimento rápido:[13]
>>> class Spam:
... def __init__(self, n): self.n = n
... def __lt__(self, other): return self.n < other.n
... def __repr__(self): return f'Spam({self.n})'
...
>>> l = [Spam(n) for n in range(5, 0, -1)]
>>> l
[Spam(5), Spam(4), Spam(3), Spam(2), Spam(1)]
>>> sorted(l)
[Spam(1), Spam(2), Spam(3), Spam(4), Spam(5)]
Isso confirma a suspeita: eu consigo passar um lista de Spam
para sort
, porque Spam
implementa __lt__
— o método especial subjacente ao operador <
.
Então o parâmetro de tipo T
no Exemplo 19 deveria ser limitado a tipos que implementam __lt__
.
No Exemplo 18, precisávamos de um parâmetro de tipo que implementava __hash__
, para poder usar typing.Hashable
como limite superior do parâmetro de tipo.
Mas agora não há um tipo adequado em typing
ou abc
para usarmos, então precisamos criar um.
O Exemplo 20 mostra o novo tipo SupportsLessThan
, um Protocol
.
Protocol
, SupportsLessThan
link:code/08-def-type-hints/comparable/comparable.py[role=include]
-
Um protocolo é uma subclasse de
typing.Protocol
. -
O corpo do protocolo tem uma ou mais definições de método, com
…
em seus corpos.
Um tipo T
é consistente-com um protocolo P
se T
implementa todos os métodos definido em P
, com assinaturas de tipo correspondentes.
Dado SupportsLessThan
, nós agora podemos definir essa versão funcional de top
no Exemplo 21.
top
usando uma TypeVar
com bound=SupportsLessThan
link:code/08-def-type-hints/comparable/top.py[role=include]
Vamos testar top
. O Exemplo 22 mostra parte de uma bateria de testes para uso com o pytest
.
Ele tenta chamar top
primeiro com um gerador de expressões que produz tuple[int, str]
, e depois com uma lista de object
.
Com a lista de object
, esperamos receber uma exceção de TypeError
.
top
link:code/08-def-type-hints/comparable/top_test.py[role=include]
# muitas linhas omitidas
link:code/08-def-type-hints/comparable/top_test.py[role=include]
-
A constante
typing.TYPE_CHECKING
é sempreFalse
durante a execução do programa, mas os verificadores de tipo fingem que ela éTrue
quando estão fazendo a verificação. -
Declaração de tipo explícita para a variável
series
, para tornar mais fácil a leitura da saída do Mypy.[14] -
Esse
if
evita que as três linhas seguintes sejam executadas durante o teste. -
reveal_type()
não pode ser chamada durante a execução, porque não é uma função regular, mas sim um mecanismo de depuração do Mypy - por isso não háimport
para ela. Mypy vai produzir uma mensagem de depuração para cada chamada à pseudo-funçãoreveal_type()
, mostrando o tipo inferido do argumento. -
Essa linha será marcada pelo Mypy como um erro.
Os testes anteriores são bem sucedidos - mas eles funcionariam de qualquer forma, com ou sem dicas de tipo em top.py.
Mais precisamente, se eu verificar aquele arquivo de teste com o Mypy, verei que o TypeVar
está funcionando como o esperado.
Veja a saída do comando mypy
no Exemplo 23.
Warning
|
Desde o Mypy 0.910 (julho de 2021), em alguns casos a saída de |
…/comparable/ $ mypy top_test.py
top_test.py:32: note:
Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]" (1)
top_test.py:33: note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test.py:34: note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]" (2)
top_test.py:41: note:
Revealed type is "builtins.list[builtins.object*]" (3)
top_test.py:43: error:
Value of type variable "LT" of "top" cannot be "object" (4)
Found 1 error in 1 file (checked 1 source file)
-
Em
test_top_tuples
,reveal_type(series)
mostra que ele é umIterator[tuple[int, str]]
— que eu declarei explicitamente. -
reveal_type(result)
confirma que o tipo produzido pela chamada atop
é o que eu queria: dado o tipo deseries
, oresult
élist[tuple[int, str]]
. -
Em
test_top_objects_error
,reveal_type(series)
mostra que ele é umalist[object*]
. Mypy põe um*
após qualquer tipo que tenha sido inferido: eu não anotei o tipo deseries
nesse teste. -
Mypy marca o erro que esse teste produz intencionalmente: o tipo dos elementos do
Iterable
series
não pode serobject
(ele tem que ser do tipoSupportsLessThan
).
A principal vantagem de um tipo protocolo sobre os ABCs é que o tipo não precisa de nenhuma declaração especial para ser consistente-com um tipo protocolo. Isso permite que um protocolo seja criado aproveitando tipos pré-existentes, ou tipos implementados em bases de código que não estão sob nosso controle.
Eu não tenho que derivar ou registrar str
, tuple
, float
, set
, etc. com SupportsLessThan
para usá-los onde um parâmetro SupportsLessThan
é esperado.
Eles só precisam implementar __lt__
.
E o verificador de tipo ainda será capaz de realizar seu trabalho, porque SupportsLessThan
está explicitamente declarado como um Protocol
— diferente dos protocolos implícitos comuns no duck typing, que são invisíveis para o verificador de tipos.
A classe especial Protocol
foi introduzida na
PEP 544—Protocols: Structural subtyping (static duck typing).
O Exemplo 21 demonstra porque esse recurso é conhecido como duck typing estático (static duck typing): a solução para anotar o parâmetro series
de top
era dizer "O tipo nominal de series
não importa, desde que ele implemente o método __lt__
."
Em Python, o duck typing sempre permitiu dizer isso de forma implícita, deixando os verificadores de tipo estáticos sem ação.
Um verificador de tipo não consegue ler o código fonte em C do CPython, ou executar experimentos no console para descobrir que sorted
só requer que seus elementos suportem <
.
Agora podemos tornar o duck typing explícito para os verificadores estáticos de tipo. Por isso faz sentido dizer que typing.Protocol
nos oferece duck typing estático.[15]
Há mais para falar sobre typing.Protocol
. Vamos voltar a ele na Parte IV, onde [ifaces_prot_abc] compara as abordagens da tipagem estrutural, do duck typing e dos ABCs - outro modo de formalizar protocolos.
Além disso, a [overload_sec] (no [more_types_ch]) explica como declarar assinaturas de funções de sobrecarga (overload) com @typing.overload
, e inclui um exemplo bastante extenso usando typing.Protocol
e uma TypeVar
delimitada.
Note
|
O |
Para anotar parâmetros de callback ou objetos callable retornados por funções de ordem superior, o módulo collections.abc
oferece o tipo Callable
, disponível no módulo typing
para quem ainda não estiver usando Python 3.9.
Um tipo Callable
é parametrizado assim:
Callable[[ParamType1, ParamType2], ReturnType]
A lista de parâmetros - [ParamType1, ParamType2]
— pode ter zero ou mais tipos.
Aqui está um exemplo no contexto de uma função repl
, parte do interpretador iterativo simples que veremos na [pattern_matching_case_study_sec]:[16]
def repl(input_fn: Callable[[Any], str] = input]) -> None:
Durante a utilização normal, a função repl
usa a input
nativa do Python para ler expressões inseridas pelo usuário.
Entretanto, para testagem automatizada ou para integração com outras fontes de input, repl
aceita um parâmetro input_fn
opcional:
um Callable
com o mesmo parâmetro e tipo de retorno de input
.
A input
nativa tem a seguinte assinatura no typeshed:
def input(__prompt: Any = ...) -> str: ...
A assinatura de input
é consistente-com esta dica de tipo Callable
Callable[[Any], str]
Não existe sintaxe para a nomear tipo de argumentos opcionais ou de palavra-chave. A
documentação de typing.Callable
diz "tais funções são raramente usadas como tipo de callback." Se você precisar de um dica de tipo para acompanhar uma função com assinatura flexível, substitua o lista de parâmetros inteira por …
- assim:
Callable[..., ReturnType]
A interação de parâmetros de tipo genéricos com uma hierarquia de tipos introduz um novo conceito: variância.
Imagine um sistema de controle de temperatura com uma função update
simples, como mostrada no Exemplo 24.
A função update
chama a função probe
para obter a temperatura atual, e chama display
para mostrar a temperatura para o usuário.
probe
e display
são ambas passadas como argumentos para update
, por motivos didáticos. O objetivo do exemplo é contrastar duas anotações de Callable
: uma com um tipo de retorno e outro com um tipo de parâmetro.
link:code/08-def-type-hints/callable/variance.py[role=include]
-
update
recebe duas funções callable como argumentos. -
probe
precisa ser uma callable que não recebe nenhuma argumento e retorna umfloat
-
display
recebe um argumentofloat
e retornaNone
. -
probe_ok
é consistente-comCallable[[], float]
porque retornar umint
não quebra código que espera umfloat
. -
display_wrong
não é consistente-comCallable[[float], None]
porque não há garantia que uma função esperando umint
consiga lidar com umfloat
; por exemplo, a funçãohex
do Python aceita umint
mas rejeita umfloat
. -
O Mypy marca essa linha porque
display_wrong
é incompatível com a dica de tipo no parâmetrodisplay
emupdate
. -
display_ok
é consistente_comCallable[[float], None]
porque uma função que aceita umcomplex
também consegue lidar com um argumentofloat
. -
Mypy está satisfeito com essa linha.
Resumindo,
não há problema em fornecer uma função de callback que retorne um int
quando o código espera uma função callback que retorne um float
, porque um valor int
sempre pode ser usado onde um float
é esperado.
Formalmente, dizemos que Callable[[], int]
é subtipo-de Callable[[], float]
— assim como int
é subtipo-de float
.
Isso significa que Callable
é covariante no que diz respeito aos tipos de retorno, porque a relação subtipo-de dos tipos int
e float
aponta na mesma direção que os tipo Callable
que os usam como tipos de retorno.
Por outro lado, é um erro de tipo fornecer uma função callback que recebe um argumento int
quando é necessário um callback que possa processar um float
.
Formalmente, Callable[[int], None]
não é subtipo-de Callable[[float], None]
.
Apesar de int
ser subtipo-de float
, no Callable
parametrizado a relação é invertida:
Callable[[float], None]
é subtipo-de Callable[[int], None]
.
Assim dizemos que aquele Callable
é contravariante a respeito dos tipos de parâmetros declarados.
A [variance_sec] no [more_types_ch] explica variância em mais detalhes e com exemplos de tipos invariantes, covariantes e contravariantes.
Tip
|
Por hora, saiba que a maioria dos tipos genéricos parametrizados são invariantes, portanto mais simples.
Por exemplo, se eu declaro
|
Agora chegamos ou último tipo especial que examinaremos nesse capítulo.
Esse é um tipo especial usado apenas para anotar o tipo de retorno de funções que nunca retornam. Normalmente, elas existem para gerar exceções. Há dúzias dessas funções na biblioteca padrão.
Por exemplo, sys.exit()
levanta SystemExit
para encerrar o processo Python.
Sua assinatura no typeshed
é:
def exit(__status: object = ...) -> NoReturn: ...
O parâmetro __status__
é apenas posicional, e tem um valor default.
Arquivos stub não contém valores default, em vez disso eles usam …
.
O tipo de __status
é object
, o que significa que pode também ser None
,
assim seria redundante escrever Optional[object]
.
Na [class_metaprog],
o [checked_class_bottom_ex] usa NoReturn
em __flag_unknown_attrs
, um método projetado para produzir uma mensagem de erro completa e amigável, e então levanta um AttributeError
.
A última seção desse capítulo épico é sobre parâmetros posicionais e variádicos
Lembra da função tag
do [tagger_ex]?
Da última vez que vimos sua assinatura foi em [positional_only_params]:
def tag(name, /, *content, class_=None, **attrs):
Aqui está tag
, completamente anotada e ocupando várias linhas - uma convenção comum para assinaturas longas,
com quebras de linha como o formatador blue faria:
from typing import Optional
def tag(
name: str,
/,
*content: str,
class_: Optional[str] = None,
**attrs: str,
) -> str:
Observe a dica de tipo *content: str
, para parâmetros posicionais arbitrários;
Isso significa que todos aqueles argumentos tem que ser do tipo str
.
O tipo da variável local content
no corpo da função será tuple[str, …]
.
A dica de tipo para argumentos de palavra-chave arbitrários é attrs: str
neste exemplo, portanto o tipo de attrs
dentro da função será dict[str, str]
.
Para uma dica de tipo como attrs: float
,
o tipo de attrs
na função seria dict[str, float]
.``
Se for necessário que o parâmetro attrs
aceite valores de tipos diferentes, é preciso usar uma Union[]
ou Any
: **attrs: Any
.
A notação /
para parâmetros puramente posicionais só está disponível com Python ≥ 3.8.
Em Python 3.7 ou anterior, isso é um erro de sintaxe.
A convenção da PEP 484 é prefixar o nome cada parâmetro puramente posicional com dois sublinhados.
Veja a assinatura de tag
novamente, agora em duas linhas, usando a convenção da PEP 484:
from typing import Optional
def tag(__name: str, *content: str, class_: Optional[str] = None,
**attrs: str) -> str:
O Mypy entende e aplica as duas formas de declarar parâmetros puramente posicionais.
Para encerrar esse capítulo, vamos considerar brevemente os limites das dicas de tipo e do sistema de tipagem estática que elas suportam.
Os mantenedores de grandes bases de código corporativas relatam que muitos bugs são encontrados por verificadores de tipo estáticos, e o custo de resolvê-los é menor que se os mesmos bugs fossem descobertos apenas após o código estar rodando em produção. Entretanto, é essencial observar que a testagem automatizada era uma prática padrão largamente adotada muito antes da tipagem estática ser introduzida nas empresas que eu conheço.
Mesmo em contextos onde ela é mais benéfica, a tipagem estática não pode ser elevada a árbitro final da correção. Não é difícil encontrar:
- Falsos Positivos
-
Ferramentas indicam erros de tipagem em código correto.
- Falsos Negativos
-
Ferramentas não indicam erros em código incorreto.
Além disso, se formos forçados a checar o tipo de tudo, perdemos um pouco do poder expressivo do Python:
-
Alguns recursos convenientes não podem ser checados de forma estática: por exemplo, o desempacotamento de argumentos como em
config(**settings)
. -
Recursos avançados como propriedades, descritores, metaclasses e metaprogramação em geral, têm suporte muito deficiente ou estão além da compreensão dos verificadores de tipo
-
Verificadores de tipo ficam obsoletos e/ou incompatíveis após o lançamento de novas versões do Python, rejeitando ou mesmo quebrando ao analisar código com novos recursos da linguagem - algumas vezes por mais de um ano.
Restrições comuns de dados não podem ser expressas no sistema de tipo - mesmo restrições simples. Por exemplo, dicas de tipo são incapazes de assegurar que "quantidade deve ser um inteiro > 0" ou que "label deve ser uma string com 6 a 12 letras em ASCII." Em geral, dicas de tipo não são úteis para localizar erros na lógica do negócio subjacente ao código.
Dadas essas ressalvas, dicas de tipo não podem ser o pilar central da qualidade do software, e torná-las obrigatórias sem qualquer exceção só amplificaria os aspectos negativos.
Considere o verificador de tipo estático como uma das ferramentas na estrutura moderna de integração de código, ao lado de testadores, analisadores de código (linters), etc. O objetivo de uma estrutura de produção de integração de código é reduzir as falhas no software, e testes automatizados podem encontrar muitos bugs que estão fora do alcance de dicas de tipo. Qualquer código que possa ser escrito em Python pode ser testado em Python - com ou sem dicas de tipo.
Note
|
O título e a conclusão dessa seção foram inspirados pelo artigo "Strong Typing vs. Strong Testing" (EN) de Bruce Eckel, também publicado na antologia The Best Software Writing I (EN), editada por Joel Spolsky (Apress). Bruce é um fã de Python, e autor de livros sobre C++, Java, Scala, e Kotlin. Naquele texto, ele conta como foi um defensor da tipagem estática até aprender Python, e conclui: "Se um programa em Python tem testes de unidade adequados, ele poderá ser tão robusto quanto um programa em C++, Java, ou C# com testes de unidade adequados (mas será mais rápido escrever os testes em Python). |
Isso encerra nossa cobertura das dicas de tipo em Python por agora. Elas serão também o ponto central do [more_types_ch], que trata de classes genéricas, variância, assinaturas sobrecarregadas, coerção de tipos (type casting), entre outros tópicos. Até lá, as dicas de tipo aparecerão em várias funções ao longo do livro.
Começamos com uma pequena introdução ao conceito de tipagem gradual, depois adotamos uma abordagem prática. É difícil ver como a tipagem gradual funciona sem uma ferramenta que efetivamente leia as dicas de tipo, então desenvolvemos uma função anotada guiados pelos relatórios de erro do Mypy.
Voltando à ideia de tipagem gradual, vimos como ela é um híbrido do duck typing tradicional de Python e da tipagem nominal mais familiar aos usuários de Java, C++ e de outra linguagens de tipagem estática.
A maior parte do capítulo foi dedicada a apresentar os principais grupos de tipos usados em anotações.
Muitos dos tipos discutidos estão relacionados a tipos conhecidos de objetos do Python, tais como coleções, tuplas e callables - estendidos para suportar notação genérica do tipo Sequence[float]
.
Muitos daqueles tipos são substitutos temporários, implementados no módulo typing
antes que os tipos padrão fossem modificados para suportar genéricos, no Python 3.9.
Alguns desses tipos são entidade especiais.
Any
, Optional
, Union
, e NoReturn
não tem qualquer relação com objetos reais na memória, existem apenas no domínio abstrato do sistema de tipos.
Estudamos genéricos parametrizados e variáveis de tipo, que trazem mais flexibilidade para as dicas de tipo sem sacrificar a segurança da tipagem.
Genéricos parametrizáveis se tornam ainda mais expressivos com o uso de Protocol
.
Como só surgiu no Python 3.8, Protocol
ainda não é muito usado - mas é de uma enorme importância.
Protocol
permite duck typing estático:
É a ponte fundamental entre o núcleo do Python, coberto pelo duck typing, e a tipagem nominal que permite a verificadores de tipo estáticos encontrarem bugs.
Ao discutir alguns desses tipos, usamos o Mypy para localizar erros de checagem de tipo e tipos inferidos, com a ajuda da função mágica reveal_type()
do Mypy.
A seção final mostrou como anotar parâmetros exclusivamente posicionais e variádicos.
Dicas de tipo são um tópico complexo e em constante evolução. Felizmente elas são um recurso opcional. Vamos manter o Python acessível para a maior base de usuários possível, e parar de defender que todo código Python precisa ter dicas de tipo - como já presenciei em sermões públicos de evangelistas da tipagem.
Nosso BDFL[17] emérito liderou a movimento de inclusão de dicas de tipo em Python, então é muito justo que esse capítulo comece e termine com palavras dele.
Não gostaria de uma versão de Python na qual eu fosse moralmente obrigado a adicionar dicas de tipo o tempo todo. Eu realmente acho que dicas de tipo tem seu lugar, mas há muitas ocasiões em que elas não valem a pena, e é maravilhoso que possamos escolher usá-las.[18]
Bernát Gábor escreveu em seu excelente post, "The state of type hints in Python" (EN):
Dicas de Tipo deveriam ser usadas sempre que valha à pena escrever testes de unidade .
Eu sou um grande fã de testes, mas também escrevo muito código exploratório. Quando estou explorando, testes e dicas de tipo não ajudam. São um entrave.
Esse post do Gábor é uma das melhores introduções a dicas de tipo em Python que eu já encontrei, junto com o texto de Geir Arne Hjelle, "Python Type Checking (Guide)" (EN). "Hypermodern Python Chapter 4: Typing" (EN), de Claudio Jolowicz, é uma introdução mas curta que também fala de validação de checagem de tipo durante a execução.
Para uma abordagem mais aprofundada, a documentação do Mypy é a melhor fonte. Ela é útil independente do verificador de tipo que você esteja usando, pois tem páginas de tutorial e de referência sobre tipagem em Python em geral - não apenas sobre o próprio Mypy.
Lá você também encontrará uma conveniente página de referência (ou _cheat sheet) (EN) e uma página muito útil sobre problemas comuns e suas soluções (EN).
A documentação do módulo typing
é uma boa referência rápida, mas não entra em muitos detalhes.
A PEP 483—The Theory of Type Hints (EN) inclui uma explicação aprofundada sobre variância, usando Callable
para ilustrar a contravariância.
As referências definitivas são as PEP relacionadas a tipagem.
Já existem mais de 20 delas.
A audiência alvo das PEPs são os core developers (desenvolvedores principais da linguagem em si) e o Steering Council do Python, então elas pressupõe uma grande quantidade de conhecimento prévio, e certamente não são uma leitura leve.
Como já mencionado, o [more_types_ch] cobre outros tópicos sobre tipagem, e a [more_type_hints_further_sec] traz referências adicionais, incluindo a [typing_peps_tbl], com a lista das PEPs sobre tipagem aprovadas ou em discussão até o final de 2021.
"Awesome Python Typing" é uma ótima coleção de links para ferramentas e referências.
Apenas Pedale
Esqueça as desconfortáveis bicicletas ultraleves, as malhas brilhantes, os sapatos desajeitados que se prendem a pedais minúsculos, o esforço de quilômetros intermináveis. Em vez disso, faça como você fazia quando era criança - suba na sua bicicleta e descubra o puro prazer de pedalar.
Just Ride: A Radically Practical Guide to Riding Your Bike (Apenas Pedale: Um Guia Radicalmente Prático sobre o Uso de sua Bicicleta) (Workman Publishing)
Se programar não é sua profissão principal, mas uma ferramenta útil no seu trabalho ou algo que você faz para aprender, experimentar e se divertir, você provavelmente não precisa de dicas de tipo mais que a maioria dos ciclistas precisa de sapatos com solas rígidas e presilhas metálicas.
Apenas programe.
O Efeito Cognitivo da Tipagem
Eu me preocupo com o efeito que as dicas de tipo terão sobre o estilo de programação em Python.
Concordo que usuários da maioria das APIs se beneficiam de dicas de tipo. Mas o Python me atraiu - entre outras razões - porque proporciona funções tão poderosas que substituem APIs inteiras, e podemos escrever nós mesmos funções poderosas similares.
Considere a função nativa max()
.
Ela é poderosa, entretanto fácil de entender. Mas vou mostrar na [max_overload_sec] que são necessárias 14 linhas de dicas de tipo para anotar corretamente essa função - sem contar um typing.Protocol
e algumas definições de TypeVar
para sustentar aquelas dicas de tipo.
Me inquieta que a coação estrita de dicas de tipo em bibliotecas desencorajem programadores de sequer considerarem programar funções assim no futuro.
De acordo com o verbete em inglês na Wikipedia, "relatividade linguística" — ou a hipótese Sapir–Whorf — é um "princípio alegando que a estrutura de uma linguagem afeta a visão de mundo ou a cognição de seus falantes"
A Wikipedia continua:
-
A versão forte diz que a linguagem determina o pensamento, e que categorias linguísticas limitam e determinam as categorias cognitivas.
-
A versão fraca diz que as categorias linguísticas e o uso apenas influenciam o pensamento e as decisões.
Linguistas em geral concordam que a versão forte é falsa, mas há evidência empírica apoiando a versão fraca.
Não conheço estudos específicos com linguagens de programação, mas na minha experiência, elas tiveram grande impacto sobre a forma como eu abordo problemas. A primeira linguagem de programação que usei profissionalmente foi o Applesoft BASIC, na era dos computadores de 8 bits. Recursão não era diretamente suportada pelo BASIC - você tinha que produzir sua própria pilha de chamada (call stack) para obter recursão. Então eu nunca considerei usar algoritmos ou estruturas de dados recursivos. Eu sabia, em algum nível conceitual, que tais coisas existiam, mas elas não eram parte de meu arsenal de técnicas de resolução de problemas.
Décadas mais tarde, quando aprendi Elixir, gostei de resolver problemas com recursão e usei essa técnica além da conta - até descobrir que muitas das minhas soluções seriam mais simples se que usasse funções existentes nos módulos Enum
e Stream
do Elixir.
Aprendi que código de aplicações em Elixir idiomático raramente contém chamadas recursivas explícitas - em vez disso, usam enums e streams que implementam recursão por trás das cortinas.
A relatividade linguística pode explicar a ideia recorrente (e também não provada) que aprender linguagens de programação diferentes torna alguém um programador melhor, especialmente quando as linguagens em questão suportam diferentes paradigmas de programação. Praticar com Elixir me tornou mais propenso a aplicar patterns funcionais quando escrevo programas em Python ou Go.
Agora voltando à Terra.
O pacote requests
provavelmente teria uma API muito diferente se Kenneth Reitz estivesse decidido (ou tivesse recebido ordens de seu chefe) a anotar todas as suas funções. Seu objetivo era escrever uma API que fosse fácil de usar, flexível e poderosa. Ele conseguiu, dada a fantástica popularidade de requests
- em maio de 2020, ela estava em #4 nas PyPI Stats, com 2,6 milhões de downloads diários. A #1 era a urllib3
, uma dependência de requests
.
Em 2017 os mantenedores de requests
decidiram não perder seu tempo escrevendo dicas de tipo. Um deles, Cory Benfield, escreveu um email dizendo:
Acho que bibliotecas com APIs 'pythônicas' são as menos propensas a adotar esse sistema de tipagem, pois ele vai adicionar muito pouco valor a elas.
Naquela mensagem, Benfield incluiu esse exemplo extremo de uma tentativa de definição de tipo para o
argumento nomeado files
em requests.request()
:
Optional[ Union[ Mapping[ basestring, Union[ Tuple[basestring, Optional[Union[basestring, file]]], Tuple[basestring, Optional[Union[basestring, file]], Optional[basestring]], Tuple[basestring, Optional[Union[basestring, file]], Optional[basestring], Optional[Headers]] ] ], Iterable[ Tuple[ basestring, Union[ Tuple[basestring, Optional[Union[basestring, file]]], Tuple[basestring, Optional[Union[basestring, file]], Optional[basestring]], Tuple[basestring, Optional[Union[basestring, file]], Optional[basestring], Optional[Headers]] ] ] ] ]
E isso assume essa definição:
Headers = Union[ Mapping[basestring, basestring], Iterable[Tuple[basestring, basestring]], ]
Você acha que requests
seria como é se os mantenedores insistissem em ter uma cobertura de dicas de tipo de 100%?
SQLAlchemy é outro pacote importante que não trabalha muito bem com dicas de tipo.
O que torna essas bibliotecas fabulosas é incorporarem a natureza dinâmica do Python.
Apesar das dicas de tipo trazerem benefícios, há também um preço a ser pago.
Primeiro, há o significativo investimento em entender como o sistema de tipos funciona. Esse é um custo unitário.
Mas há também um custo recorrente, eterno.
Nós perdemos um pouco do poder expressivo do Python se insistimos que tudo precisa estar sob a checagem de tipos.
Recursos maravilhosos como desempacotamento de argumentos — e.g., config(**settings)
— estão além da capacidade de compreensão dos verificadores de tipo.
Se você quiser ter uma chamada como config(**settings)
verificada quanto ao tipo, você precisa explicitar cada argumento.
Isso me traz lembranças de programas em Turbo Pascal, que escrevi 35 anos atrás.
Bibliotecas que usam metaprogramação são difíceis ou impossíveis de anotar. Claro que a metaprogramação pode ser mal usada, mas isso também é algo que torna muitos pacotes do Python divertidos de usar.
Se dicas de tipo se tornarem obrigatórias sem exceções, por uma decisão superior em grande empresas, aposto que logo veremos pessoas usando geração de código para reduzir linhas de código padrão em programas Python - uma prática comum com linguagens menos dinâmicas.
Para alguns projetos e contextos, dicas de tipo simplesmente não fazem sentido. Mesmo em contextos onde elas fazer muito sentido, não fazem sentido o tempo todo. Qualquer política razoável sobre o uso de dicas de tipo precisa conter exceções.
Alan Kay, o recipiente do Turing Award que foi um dos pioneiros da programação orientada a objetos, certa vez disse:
Algumas pessoas são completamente religiosas no que diz respeito a sistemas de tipo, e como um matemático eu adoro a ideia de sistemas de tipos, mas ninguém até agora inventou um que tenha alcance o suficiente..[19]
Obrigado, Guido, pela tipagem opcional. Vamos usá-la como foi pensada, e não tentar anotar tudo em conformidade estrita com um estilo de programação que se parece com Java 1.5.
Duck Typing FTW
Duck typing encaixa bem no meu cérebro, e duck typing estático é um bom compromisso, permitindo checagem estática de tipo sem perder muito da flexibilidade que alguns sistemas de tipagem nominal só permitem ao custo de muita complexidade - isso quando permitem.
Antes da PEP 544, toda essa ideia de dicas de tipo me parecia completamente não-pythônica,
Fiquei muito feliz quando vi typing.Protocol
surgir em Python.
Ele traz equilíbrio para a Força.
Genéricos ou Específicos?
De uma perspectiva de Python, o uso do termo "genérico" na tipagem é um retrocesso. Os sentidos comuns do termo "genérico" são "aplicável integralmente a um grupo ou uma classe" ou "sem uma marca distintiva."
Considere list
versus list[str]
. o primeiro é genérico: aceita qualquer objeto. O segundo é específico: só aceita str
.
Por outro lado, o termo faz sentido em Java.
Antes do Java 1.5, todas as coleções de Java (exceto a mágica array
) eram "específicas": só podiam conter referência a Object
, então era necessário converter os itens que saim de uma coleção antes que eles pudessem ser usados. Com Java 1.5, as coleções ganharam parâmetros de tipo, e se tornaram "genéricas."
Enum
. Por exemplo, não é possível, usando dicas de tipo, definir Quantity
como um número inteiro entre 1 e 10000, ou AirportCode
como uma sequência de 3 letras. O NumPy oferece uint8
, int16
, e outros tipos numéricos referentes à arquitetura do hardware, mas na biblioteca padrão do Python nós encontramos apenas tipos com conjuntos muitos pequenos de valores (NoneType
, bool
) ou conjuntos muito grandes (float
, int
, str
, todas as tuplas possíveis, etc.).
typing.Protocol
. Vamos falar disso mais adiante nesse capítulo - em Protocolos estáticos — e com mais detalhes em [ifaces_prot_abc].
ord
só aceita str
ou bytes
com len(s) == 1
. Mas no momento o sistema de tipagem não consegue expressar essa restrição.
typing
foi acrescentar dúzias de avisos de descontinuação, enquanto eu reorganizava as entradas abaixo de "Conteúdo do Módulo" em subseções, sob a supervisão de Guido van Rossum.
:=
quando faz sentido em alguns exemplos, mas não trato desse operador no livro. Veja PEP 572—Assignment Expressions para entender os detalhes dos operadores de atribuição.
dict
é uma subclasse virtual de abc.MutableMapping
. O conceito de subclasse virtual será explicado em [ifaces_prot_abc]. Por hora, basta saber que issubclass(dict, abc.MutableMapping)
é True
, apesar de dict ser implementado em C e não herdar nada de abc.MutableMapping
, apenas de object
.
statistics
na biblioteca padrão do Python
typeshed
, e em 26 de maio de 2020 mode
aparecia anotado assim em statistics.pyi.
series
como Generator[Tuple[builtins.int, builtins.str*], None, None]
, que é prolixo mas consistente-com Iterator[tuple[int, str]]
, como veremos na [generic_iterable_types_sec].