1. Introdução

Ilustração de Magdalena Tomczyk
Segunda parte
Python é uma linguagem com digitação dinâmica e permite manipular livremente variáveis de diferentes tipos. Entretanto, ao escrever código, de uma forma ou de outra, assumimos que tipos de variáveis serão usados (isso pode ser causado por uma restrição do algoritmo ou da lógica de negócios). E para o correto funcionamento do programa, é importante que encontremos erros o mais cedo possível, associados à transferência de dados do tipo errado.
Mantendo a idéia de um pato de digitação dinâmico nas versões modernas do Python (3.6+), ele suporta anotações de tipos de variáveis, campos de classes, argumentos e valores de retorno de funções:
As anotações de tipo são simplesmente lidas pelo interpretador Python e não são mais processadas, mas estão disponíveis para uso em códigos de terceiros e são projetadas principalmente para uso por analisadores estáticos.
Meu nome é Andrey Tikhonov e estou envolvido no desenvolvimento de back-end em Lamoda.
Neste artigo, quero explicar o básico do uso de anotações de tipo e considerar os exemplos típicos implementados ao typing
anotações.
Ferramentas de anotação
As anotações de tipo são suportadas por muitos IDEs do Python que destacam código incorreto ou fornecem dicas enquanto você digita.
Por exemplo, é assim que fica no Pycharm:
Erro ao destacar

Dicas:

As anotações de tipo também são processadas pelos linters do console.
Aqui está a saída do pylint:
$ pylint example.py ************* Module example example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member)
Mas para o mesmo arquivo que mypy encontrou:
$ mypy example.py example.py:7: error: "int" has no attribute "startswith" example.py:10: error: Unsupported operand types for // ("str" and "int")
O comportamento de diferentes analisadores pode variar. Por exemplo, mypy e pycharm tratam de alterar o tipo de uma variável de maneira diferente. Ainda nos exemplos, vou focar na saída do mypy.
Em alguns exemplos, o código pode ser executado sem exceção na inicialização, mas pode conter erros lógicos devido ao uso de variáveis do tipo errado. E em alguns exemplos, pode nem ser executado.
O básico
Diferentemente das versões mais antigas do Python, as anotações de tipo não são escritas em comentários ou docstring, mas diretamente no código. Por um lado, isso quebra a compatibilidade descendente, por outro lado, significa claramente que faz parte do código e pode ser processado de acordo.
No caso mais simples, a anotação contém o tipo diretamente esperado. Casos mais complexos serão discutidos abaixo. Se a classe base for especificada como uma anotação, é aceitável passar instâncias de seus descendentes como valores. No entanto, você pode usar apenas os recursos implementados na classe base.
As anotações para variáveis são escritas após os dois pontos após o identificador. Depois disso, o valor pode ser inicializado. Por exemplo
price: int = 5 title: str
Os parâmetros de função são anotados da mesma maneira que as variáveis, e o valor de retorno é indicado após a seta ->
e antes dos dois pontos finais. Por exemplo
def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s
Para campos de classe, as anotações devem ser especificadas explicitamente ao definir uma classe. No entanto, os analisadores podem produzi-los automaticamente com base no método __init__
, mas nesse caso eles não estarão disponíveis no tempo de execução. Leia mais sobre como trabalhar com anotações em tempo de execução na segunda parte do artigo
class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury')
A propósito, ao usar a classe de dados, os tipos de campo devem ser especificados na classe. Mais sobre classe de dados
Tipos incorporados
Embora você possa usar tipos padrão como anotações, muitas coisas úteis estão ocultas no módulo de typing
.
Opcional
Se você marcar a variável com o tipo int
e tentar atribuí-la como None
, haverá um erro:
Incompatible types in assignment (expression has type "None", variable has type "int")
Para esses casos, uma anotação Optional
com um tipo específico é fornecida no módulo de digitação. Observe que o tipo de variável opcional é indicado entre colchetes.
from typing import Optional amount: int amount = None
Qualquer
Às vezes, você não deseja limitar os tipos possíveis de uma variável. Por exemplo, se isso realmente não é importante, ou se você planeja fazer o processamento de diferentes tipos por conta própria. Nesse caso, você pode usar a anotação Any
. Mypy não jura pelo seguinte código:
unknown_item: Any = 1 print(unknown_item) print(unknown_item.startswith("hello")) print(unknown_item // 0)
A questão pode surgir: por que não usar object
? No entanto, nesse caso, supõe-se que, embora qualquer objeto possa ser transferido, ele só pode ser acessado como uma instância do object
.
unknown_object: object print(unknown_object) print(unknown_object.startswith("hello"))
União
Nos casos em que é necessário permitir o uso de não apenas alguns tipos, mas apenas alguns, você pode usar a anotação de typing.Union
com uma lista de tipos entre colchetes.
def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100")
A propósito, a anotação Optional[T]
é equivalente a Union[T, None]
, embora essa entrada não seja recomendada.
Colecções
O mecanismo de anotação de tipo suporta o mecanismo genérico ( Genéricos , mais na segunda parte do artigo), que permite especificar os tipos de elementos armazenados neles para contêineres.
Listas
Para indicar que uma variável contém uma lista, você pode usar o tipo de lista como uma anotação. No entanto, se você desejar especificar quais elementos a lista contém, essa anotação não será mais adequada. Há typing.List
para isso. Semelhante à maneira como especificamos o tipo de uma variável opcional, especificamos o tipo de itens da lista entre colchetes.
titles: List[str] = ["hello", "world"] titles.append(100500)
Supõe-se que a lista contenha um número indefinido de elementos do mesmo tipo. Mas não há restrições na anotação de um elemento: você pode usar Any
, Optional
, List
e outros. Se o tipo de item não for especificado, será assumido como Any
.
Além da lista, anotações semelhantes são para conjuntos: typing.Set
e typing.FrozenSet
.
Tuplas
As tuplas, diferentemente das listas, são frequentemente usadas para elementos heterogêneos. A sintaxe é semelhante com uma diferença: os colchetes indicam o tipo de cada elemento da tupla individualmente.
Se você planeja usar uma tupla semelhante à lista: armazene um número desconhecido de elementos do mesmo tipo, use as reticências ( ...
).
Anotação Tuple
sem especificar os tipos de elemento funciona de maneira semelhante à Tuple[Any, ...]
price_container: Tuple[int] = (1,) price_container = ("hello")
Dicionários
Para dicionários, typing.Dict
usado. O tipo de chave e o tipo de valor são anotados separadamente:
book_authors: Dict[str, str] = {"Fahrenheit 451": "Bradbury"} book_authors["1984"] = 0
Utilizado de forma semelhante, typing.DefaultDict
e typing.OrderedDict
Função Resultado
Você pode usar qualquer anotação para indicar o tipo de resultado da função. Mas existem alguns casos especiais.
Se uma função não retornar nada (por exemplo, como print
), seu resultado será sempre None
. Para anotação, também usamos None
.
As opções válidas para finalizar essa função seriam: retornar explicitamente None
, retornar sem especificar um valor e terminar sem chamar return
.
def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return ""
Se a função nunca retornar (por exemplo, como sys.exit
), você deve usar a anotação NoReturn
:
def forever() -> NoReturn: while True: pass
Se for uma função de gerador, ou seja, seu corpo contiver uma yield
, você poderá usar a Iterable[T]
ou Generator[YT, ST, RT]
para a função retornada:
def generate_two() -> Iterable[int]: yield 1 yield "2"
Em vez de uma conclusão
Para muitas situações, existem tipos adequados no módulo de digitação, no entanto, não considerarei tudo, pois o comportamento é semelhante aos considerados.
Por exemplo, existe um Iterator
como a versão genérica para collections.abc.Iterator
, typing.SupportsInt
para indicar que o objeto suporta o método __int__
ou Callable
para funções e objetos que suportam o método __call__
O padrão também define o formato das anotações na forma de comentários e arquivos stub que contêm informações apenas para analisadores estáticos.
No próximo artigo , gostaria de me debruçar sobre o mecanismo de trabalho de genéricos e processamento de anotações em tempo de execução.