Este é um tópico que considero um pouco mais avançado em programação, pois envolve testes de código, boas práticas de código, etc. Então se você é iniciante, não se preocupe muito com ele agora. Mas se vai prosseguir na carreira de programador, em algum momento você terá que testar código e realizar boas práticas então este assunto pode vir à tona.
Prosseguindo o tópico anterior de anotações de tipo, imaginem que recebemos uma função que pode receber vários tipos de dados como argumentos e que pode retornar vários tipos de dados também:
def recebe_e_junta_tudo(
x: [str, int, float, complex, list, tuple],
y: [str, int, float, complex, list, tuple]
) -> [str, int, float, complex, list, tuple]:
return x + y
recebe_e_junta_tudo('Sardinha', 'das PN')
Acima se vê que estamos dizendo que nossa função pode receber strings, inteiros, floats, números complexos, listas e tuplas como parâmetros e pode retornar qualquer um desses tipos.
Ficou uma sintaxe bem feia, convenhamos. E continua com aquela característica de ser somente um aviso ao programador que a função deve receber esses tipos e retornar esses tipos. Caso eu entre com x e y sendo dicionários, que não estão englobados na lista de tipos permitidos, a função vai rodar sem problema (embora o console irá gerar erro pois não é possível concatenar dicionários).
Para simplificar um pouco essa questão existe a lib do Python chamada Typing, que é interessante para tipos mais complexos como os parâmetros dessa função acima.
A lib Typing tem como função isso de informar os tipos de parâmetros e de retorno de funções. Então, imaginando uma função que receba listas e tuplas como parâmetros, posso importar esses tipos de typing:
from typing import List, Tuple
def concat_lista_ou_tupla(x: [List, Tuple], y: [List, Tuple]) -> [List, Tuple]:
return x + y
concat_lista_ou_tupla([1, 2, 3], [4, 5, 6])
Não mudou muito em relação ao comportamento da primeira função do tópico. Ainda tenho parâmetros com muitos tipos permitidos envolvidos. E ainda há ambiguidade, pois o que informei acima é que a função pode receber listas OU tuplas, e não necessariamente listas E tuplas. A lib typing simplifica isso pra gente por meio do tipo Union, que representa a união de vários tipos.
from typing import List, Tuple, Union
def concat_lista_ou_tupla(x: Union[List, Tuple], y: Union[List, Tuple]) -> Union[List, Tuple]:
return x + y
concat_lista_ou_tupla([1, 2, 3], [4, 5, 6])
Tá, com o Union eu digo que a função pode receber um ou outro mas ainda tá com uma sintaxe meio feia. E se a função for receber tudo que a primeira função do tópico pode receber, ficaria algo gigantesco:
from typing import List, Tuple, Union
def concat_lista_ou_tupla_ou_strings_ou_soma_numeros(x: Union[List, Tuple, int, float, complex, str], y: Union[List, Tuple, int, float, complex, str]) -> Union[List, Tuple, int, float, complex, str]:
return x + y
Uma definição gigantesca pra uma função que só concatena ou soma valores.
Pra casos assim, typing tem o tipo "Any", que representa qualquer coisa:
from typing import List, Tuple, Union, Any
def concat_ou_soma_qualquer_coisa(x: Any, y: Any) -> Any:
return x + y
Entretanto, quando precisamos de algo mais específico, podemos usar typing para criar novos tipos de possibilidades de parâmetros com o "Union". Uma função que concatena listas, tuplas e strings não pode receber dicionários para esse fim de concatenação. Então posso criar um tipo só com listas e tuplas e strings:
from typing import Union, Any, List, Tuple
Concatenaveis = Union[List, Tuple]
def concat_lista_ou_tupla_ou_string(x: Concatenaveis, y: Concatenaveis) -> Concatenaveis:
return x + y
Também posso criar um novo tipo com um "alias" ou apelido próprio com "TypeVar":
from typing import Union, Any, List, Tuple, TypeVar
Numeros = TypeVar('Numeros', int, float, complex)
# Função que só soma números, não deve concatenar strings nem listas nem tuplas
def soma_numeros(x: Numeros, y: Numeros) -> Numeros:
return x + y
soma_numeros(3.14, 3.14)
Mas nossas funções ainda podem fazer coisas que não queremos que elas façam. Isso é algo impossível em linguagens compiladas como C#, pois no momento da compilação já há a verificação de tipos de parâmetros e o programa não compila se tiver algo divergente.
Para ter um comportamento parecido com esse de linguagens compiladas, foi criada a lib mypy, que é um type checker. Para usá-la, precisamos instalá-la via pip:
pip install mypy
E agora as coisas ficam bem interessantes a meu ver. Vamos voltar a nossa função soma_numeros, que defini que ela só deve receber números como argumentos e rodar o mypy nela.
O mypy roda pelo terminal e não pelo python. Então em vez de rodar nosso programa, iremos digitar via terminal:
$ mypy meu_script.py
Aqui eu criei para este tópico o script 3_10.py. Então no meu caso:
(venv) 3.10$ mypy 3_10.py
Success: no issues found in 1 source file
O que foi feito acima foi que o mypy conferiu os tipos passados como type hint para a função e conferiu se na chamada (ao passar 3.14 e 3.14 como argumentos) os tipos eram tipos esperados pela minha função. Agora, se eu passar strings para essa função:
from typing import Union, Any, List, Tuple, TypeVar
Numeros = TypeVar('Numeros', int, float, complex)
def soma_numeros(x: Numeros, y: Numeros) -> Numeros:
return x + y
soma_numeros('3.14', '3.14')
Rodando o mypy agora:
(venv) 3.10$ mypy 3_10.py
3_10.py:8: error: Value of type variable "Numeros" of "soma_numeros" cannot be "str"
Found 1 error in 1 file (checked 1 source file)
Ou seja, agora eu realmente tenho uma forma de realizar checagem de tipos de funções e diminuir as possibilidades de erros em meus programas.
Se eu simplificar a função informando que ela só receberá inteiros mas passar floats como argumentos:
def soma_numeros(x: int, y: int) -> int:
return x + y
soma_numeros(3.14, 3.14)
(venv) 3.10$ mypy 3_10.py
3_10.py:4: error: Argument 1 to "soma_numeros" has incompatible type "float"; expected "int"
3_10.py:4: error: Argument 2 to "soma_numeros" has incompatible type "float"; expected "int"
Found 2 errors in 1 file (checked 1 source file)
Agora eu consigo pegar possíveis erros de tipos em passagem de parâmetros nas nossas funções usando o mypy.
Mas posso automatizar mais ainda isso aí. Posso fazer um arquivo com anotações de tipo sem precisar escrever nenhuma anotação de tipo usando monkeytype.
Monkeytype é outra lib que precisamos instalar por fora, então:
pip install monkeytype
Agora vamos apagar as anotações de tipo da nossa função de soma acima e deixá-la mais simples.
# modulo.py
def soma(x, y):
return x + y
Vamos usar esse script como módulo, então vamos criar outro script que irá importar o modulo.py como módulo:
# programa.py
from modulo import soma
soma(3.14, 3.14)
soma('Bastter', '.com')
soma(3, 4)
soma(3 + 2j, 5 + 1j)
No terminal, vamos rodar monkeytype usando como base nosso programa.py:
(venv) 3.10$ monkeytype run programa.py
Esse comando irá criar um arquivo do tipo sqlite3, ou seja, um banco de dados.
Agora a mágica acontece. Ao rodar o comando abaixo, o terminal irá nos mostrar as anotações de tipo para o módulo que foi criado:
(venv) 3.10$ monkeytype stub modulo
from typing import Union
def soma(x: Union[int, str, complex, float], y: Union[int, str, complex, float]) -> Union[int, str, complex, float]: ...
E a palavra stub como função de monkeytype não foi selecionada à toa. Stub é uma extensão de arquivos chamada "pyi" que é responsável por receber essas anotações de tipos de módulos em nossos programas python. Portanto, seria como se fosse um script com nossas funções todas anotadas.
Posso transformar esse resultado num script stub de formato pyi da seguinte forma:
(venv) 3.10$ monkeytype stub modulo > modulo_stub.pyi
Abrindo esse arquivo modulo_stub.pyi:
# modulo_stub.pyi
from typing import Union
def soma(x: Union[float, complex, int, str], y: Union[float, complex, int, str]) -> Union[float, complex, int, str]: ...
Vejam que o monkeytype já criou pra mim um arquivo bonitinho com as anotações de tipo baseadas nas chamadas que fiz da função soma logo acima. Sem eu precisar escrever nenhuma anotação.
Por fim, eu posso realizar toda a anotação de tipo no meu modulo.py da seguinte forma:
(venv) 3.10$ monkeytype apply modulo
Meu modulo.py agora vai ficar assim:
# modulo.py
from typing import Union
def soma(x: Union[int, complex, str, float], y: Union[int, complex, str, float]) -> Union[int, complex, str, float]:
return x + y
Ou seja, consegui que minha função ficasse anotada sem eu escrever nenhuma linha de anotação de código, somente usando monkeytype. Nota-se que esta pode ser uma excelente ferramenta pra ajudar a documentar código, principalmente código legado.
Todo o assunto deste tópico foi feito baseado neste vídeo:
É um excelente canal de Python que eu recomendo pra quem programa na linguagem. Basicamente aprendi tudo do assunto neste vídeo rs tinha que dar os créditos pro Eduardo, dono do canal.