Módulo de digitação Python – Use verificadores de tipo eficientemente

Introduzido desde o Python 3.5, o typing módulo do Python tenta fornecer uma maneira de sugerir tipos para ajudar verificadores de tipo estático e linters a prever erros de forma precisa.

Devido ao Python ter que determinar o tipo de objetos durante a execução, às vezes fica muito difícil para os desenvolvedores descobrir o que exatamente está acontecendo no código.

Mesmo verificadores de tipo externos como o PyCharm IDE não produzem os melhores resultados; em média, apenas prevendo corretamente erros cerca de 50% do tempo, de acordo com esta resposta no StackOverflow.

O Python tenta mitigar este problema introduzindo o que é conhecido como dica de tipo (anotação de tipo) para ajudar os verificadores de tipo externos a identificar quaisquer erros. Esta é uma boa maneira para o programador sugerir o tipo do(s) objeto(s) sendo usado, durante o próprio tempo de compilação e garantir que os verificadores de tipo funcionem corretamente.

Isso torna o código Python muito mais legível e robusto também, para outros leitores!

NOTA: Isso não faz a verificação de tipo real no momento da compilação. Se o objeto real retornado não fosse do mesmo tipo que sugerido, não haverá nenhum erro de compilação. É por isso que usamos verificadores de tipo externos, como o mypy, para identificar quaisquer erros de tipo.


Para usar o módulo typing de forma eficaz, é recomendado que você utilize um verificador/linter de tipos externo para verificar a correspondência de tipos estáticos. Um dos verificadores de tipo mais amplamente utilizados para Python é o mypy, então recomendo que você o instale antes de ler o restante do artigo.

Já cobrimos os conceitos básicos de verificação de tipos em Python. Você pode revisar este artigo primeiro.

Vamos utilizar o mypy como verificador de tipo estático neste artigo, que pode ser instalado por meio de:

pip3 install mypy

Você pode executar o mypy em qualquer arquivo Python para verificar se os tipos correspondem. Isso é como se estivesse ‘compilando’ o código Python.

mypy program.py

Após depurar os erros, você pode executar o programa normalmente usando:

python program.py

Agora que cobrimos nossos pré-requisitos, vamos tentar usar algumas das funcionalidades do módulo.


Dicas de Tipos / Anotações de Tipos

Nas funções

Podemos anotar uma função para especificar o tipo de retorno e os tipos de seus parâmetros.

def print_list(a: list) -> None:
    print(a)

Isso informa ao verificador de tipos (mypy no meu caso) que temos uma função print_list(), que receberá uma list como argumento e retornará None.

def print_list(a: list) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

Vamos rodar isso primeiro no nosso verificador de tipos mypy:

vijay@JournalDev:~ $ mypy printlist.py 
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)

Como esperado, obtemos um erro; já que a linha #5 tem o argumento como um int, em vez de uma list.

Nas Variáveis

Desde o Python 3.6, também podemos anotar os tipos de variáveis, mencionando o tipo. Mas isso não é obrigatório se você quiser que o tipo de uma variável mude antes que a função retorne.

# Anota 'raio' como sendo um float
radius: float = 1.5

# Podemos anotar uma variável sem atribuir um valor!
sample: int

# Anota 'área' para retornar um float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# Imprime todas as anotações da função usando
# o dicionário '__annotations__'
print('Dictionary of Annotations for area():', area.__annotations__)

Resultado do mypy:

vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}

Esta é a maneira recomendada de usar o mypy, fornecendo primeiro as anotações de tipo antes de usar o verificador de tipo.


Aliases de Tipo

O módulo typing nos fornece Aliases de Tipo, que são definidos atribuindo um tipo ao alias.

from typing import List

# Vetor é uma lista de valores de ponto flutuante
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)

Saída

vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]

No trecho acima, Vector é um alias que representa uma lista de valores de ponto flutuante. Podemos sugerir um tipo em um alias, que é o que o programa acima está fazendo.

A lista completa de aliases aceitáveis é dada aqui.

Vamos dar uma olhada em mais um exemplo, que verifica cada par chave:valor em um dicionário e verifica se eles correspondem ao formato nome:email.

from typing import Dict
import re

# Criar um alias chamado 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # Verificar se nome e email são strings
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # Verificar o email [email protected]
        if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
            return False
    return True


print(check_if_valid({'vijay': '[email protected]'}))
print(check_if_valid({'vijay': '[email protected]', 123: '[email protected]'}))

Saída do mypy

vijay@JournalDev:~ $ mypy validcontacts.py 
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)

Aqui, obtemos um erro estático de compilação em tempo de execução no mypy, já que o parâmetro name em nosso segundo dicionário é um número inteiro (123). Assim, os aliases são outra maneira de garantir uma verificação precisa de tipo pelo mypy.


Crie tipos de dados definidos pelo usuário usando NewType()

Podemos usar a função NewType() para criar novos tipos de dados definidos pelo usuário.

from typing import NewType

# Crie um novo tipo de usuário chamado 'StudentID' que consiste em
# um inteiro
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

O verificador de tipo estático tratará o novo tipo como se fosse uma subclasse do tipo original. Isso é útil para detectar erros lógicos.

from typing import NewType

# Crie um novo tipo de usuário chamado 'StudentID'
StudentID = NewType('StudentID', int)

def get_student_name(stud_id: StudentID) -> str:
    return str(input(f'Enter username for ID #{stud_id}:\n'))

stud_a = get_student_name(StudentID(100))
print(stud_a)

# Isso está incorreto!!
stud_b = get_student_name(-1)
print(stud_b)

Saída do mypy

vijay@JournalDev:~ $ mypy studentnames.py  
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)

O tipo Any

Este é um tipo especial, informando ao verificador de tipo estático (mypy no meu caso) que todo tipo é compatível com essa palavra-chave.

Considere nossa antiga função print_list(), agora aceitando argumentos de qualquer tipo.

from typing import Any

def print_list(a: Any) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

Agora, não haverá erros quando executarmos o mypy.

vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1

Todas as funções sem um tipo de retorno ou tipos de parâmetros usarão implicitamente Any.

def foo(bar):
    return bar

# Um verificador de tipo estático tratará o acima
# como tendo a mesma assinatura que:
def foo(bar: Any) -> Any:
    return bar

Você pode, portanto, usar Any para misturar código tipado estaticamente e dinamicamente.


Conclusão

Neste artigo, aprendemos sobre o módulo typing do Python, que é muito útil no contexto de verificação de tipo, permitindo que verificadores de tipo externos, como mypy, relatem com precisão quaisquer erros.

Isso nos fornece uma maneira de escrever código tipado estaticamente em Python, que é uma linguagem tipada dinamicamente por design!


Referências


Source:
https://www.digitalocean.com/community/tutorials/python-typing-module