Python (pode ser estaticamente) tipado? “Type Hints” e “Function Annotations“
Em um post anterior, Python tipado? Introdução às “Type Hints“, nós conversamos sobre as linguagens com tipagem estática e linguagens dinâmicas, as vantagens e desvantagens de cada uma e principalmente sobre bons motivos para implementar as Type Hints (ou “dicas de tipos”, em tradução livre) no seu código Python, deixando ele “tipado”.
Recomendo bastante que você leia o post citado antes de continuar lendo esse, mas vamos falar novamente bem brevemente sobre as Type Hints. Esse conceito foi introduzido na PEP 484 e é suportado a partir do Python versão 3.5, e nos ajudam a trazer as tais vantagens das linguagens estaticamente tipadas para o Python.
O que não falamos tanto ainda desde o último post, é sobre as Function Annotations (ou “anotações de funções”, em tradução livre). Quem traz o conceito de Function Annotations é a PEP 3107, presente desde a versão 3.0 do Python, nos apresentando uma nova sintaxe que nos permite adicionar anotações de metadados arbitrários às funções do Python.
Se você já explorou códigos de desenvolvedores mais avançados, talvez a code base da sua empresa onde os códigos são facilmente reutilizáveis (ou deveriam ser), ou até mesmo se mergulhou no seu framework favorito para entender como o código funciona, você já deve ter esbarrado com essa sintaxe. E se você não entendeu – ou se nem percebeu que essas anotações estavam lá – vai passar a entender agora!
Vamos ver como essa sintaxe funciona? Vamos lá.
Entendendo as Function Annotations do Python
Vamos pegar como exemplo uma função que tenho usado bastante para plotar em gráfico Séries Temporais. É uma função bem simples, desenvolvida pelo Laurence Moroney (sou um grande fã dele) no curso DeepLearning.AI TensorFlow Developer disponível no coursera.org.
def plot_series(time,
series,
start,
end,
xlabel=None,
ylabel=None,
legend=None,
line='-' ):
Bom, como eu disse que é uma função para plotar um gráfico, a gente consegue ter alguma ideia do que cada argumento dessa função faz, mesmo sem ver o código. Certo? Mas qual será o tipo esperado, exatamente, de cada um desses parâmetros?
Os xlabel
e ylabel
são bem intuitivos para quem usa a matplotlib.pyplot
né. Mas e time
, o que será que é? Uma lista? E series
será que é uma Pandas Series? E legend
deve ser um booleano, para mostrar a legenda se for verdadeiro e não mostrar se for falso. Mas será mesmo?
Uma forma (não muito eficiente) de ter certeza de qual o tipo de dados esperado por essa função seria analisando o código dela. Para quem conhece os objetos do tipo Pandas Series (“Séries do Pandas”, em tradução livre), vai conseguir analisar os métodos que o desenvolvedor da função usou nesse objeto e descobrir se é esse mesmo o tipo de dados esperado. Porque poderia ser do tipo Series, mas poderia ser um DataFrame também…
Essa solução não é muito eficiente, principalmente para códigos muito longos e/ou mais complexos do que estamos acostumados a lidar. Quais outras formas podemos usar para fazer com que esse código fique mais compreensível?
Se você está acostumado com o desenvolvimento de um Clean Code, códigos bem organizados e bem documentados, vai me falar: “a resposta é óbvia! Vamos colocar na docstring.”
Utilizando Docstrings no Python
O que são “Docstrings” ?
Docstrings, para quem não conhece, são as strings literais (entre sequências de três aspas) que aparecem logo no início de uma função, classe, módulo ou método. Elas são usadas para documentar o propósito da função por meio de um resumo. São normalmente colocadas na primeira linha após a definição do objeto e não são apenas comentários, elas podem ser acessadas por meio do atributo__doc__
e também podem ser exibidas como documentação pela sua IDE.
E com a Docstring, ficaria da seguinte maneira o código.
def plot_series(time,
series,
start,
end,
xlabel=None,
ylabel=None,
legend=None,
line='-' ):
"""
Visualiza dados de Séries Temporais
Args:
time (array de int) - pontos de tempo
series (array de int) - medida da variável a cada ponto
start (int) - primeiro ponto de tempo a exibir
end (int) - último ponto de tempo a exibir
xlabel (str) - rótulo do eixo x
ylabel (str) - rótulo do eixo y
legend (lista de str) - texto para exibir na legenda
line (str) - formato da linha (matplotlib)
"""
Muito melhor, né? Acabou que series
não era um objeto do Pandas e legend
não era booleano. Com essa documentação a gente consegue saber com certeza que tipo de objeto é esperado em cada parâmetro da função.
Porém, ainda não temos as outras vantagens de se ter o código Python “tipado”, como por exemplo verificar se em algum ponto do código passamos o objeto do tipo errado, ou até mesmo as dicas para autocompletar com os métodos na sua IDE.
Sintaxe das Functions Annotations no Python
Para poder usufruir dos benefícios acima – e de outros – precisamos usar as Function Annotations. Então vamos usar a sintaxe das anotações na nossa função para vermos como fica.
from typing import List, Optional
def plot_series(time: np.ndarray,
series: np.ndarray,
start: int,
end: int,
xlabel: Optional[str] = 'Eixo X'
ylabel: Optional[str] = 'Eixo Y',
legend: Optional[List[str]] = None,
line: Optional[str] = '-' ):
"""
Visualiza dados de Séries Temporais
Args:
time (array de int) - pontos de tempo
series (array de int) - medida da variável a cada ponto
start (int) - primeiro ponto de tempo a exibir
end (int) - último ponto de tempo a exibir
xlabel (str) - rótulo do eixo x
ylabel (str) - rótulo do eixo y
legend (lista de str) - texto para exibir na legenda
line (str) - formato da linha (matplotlib)
"""
Agora nós temos uma função devidamente anotada! Pode parecer estranho à primeira vista, mas vamos explicar a sintaxe e você irá se acostumar com o tempo.
Após cada parâmetro nós temos dois pontos ( :
), e após os dois pontos nós podemos inserir o tipo recebido por esse parâmetro (ou variável). Nós podemos nativamente anotar todos os tipos de variáveis built-in ( int
, float
, bool
, str
e bytes
). Para as estruturas de dados (List
, Set
, Dict
e Tuple
), precisamos importar da biblioteca typing, como na primeira linha do código acima.
Em breve vamos liberar um post falando mais sobre a biblioteca typing, mas por enquanto podemos ver a documentação e notar que ela traz outras opções de anotações, como a Optional
, que serve para anotar que determinado parâmetro é opcional para que a função funcione corretamente.
Funções com retorno
Notem também que essa nossa função é apenas para plotar um gráfico, então ela não retorna nada, não tem uma declaração de return
ao final. E como ficaria para anotar qual o tipo do retorno da função? Vamos ver, mas dessa vez com uma função mais simples e intuitiva.
def cumprimento(nome: str) -> str:
frase = 'Olá ' + nome + ', seja bem-vindo(a)!'
return frase
E podemos ver a sintaxe da “seta” para o retorno da função, ou melhor, o hífen seguido do sinal de maior que ( ->
). Após essa “seta”, nós colocamos o tipo do objeto que a função irá retornar. Simples, né?
Motivos para usar as Function Annotations
Nós exemplificamos melhor os motivos para se usar as Type Hints e Function Annotations no post Python tipado? Introdução às “Type Hints“, mas vamos passar superficialmente novamente por algumas delas aqui.
Usando esta nova sintaxe, nós podemos aproveitar algumas das diversas vantagens de uma linguagem tipada, como por exemplo usar bibliotecas de validação de tipos, como a mypy para validar com antecedência possíveis erros relacionados a variáveis assumindo tipos diferentes do que era esperado, evitando assim problemas em tempo de execução (ou pior, em produção!).
Tiramos proveito também da inteligência das IDEs como VSCode, PyCharm, e até JupyterLab, para nos mostrar dicas de código enquanto programamos. Por saberem exatamente o que uma função espera e retorna, os editores de código podem nos recomendar métodos mais comuns para cada tipo de objeto e exibir trechos das documentações. As funções de Linting ficam incríveis quando usamos essa sintaxe, vocês precisam experimentar!
Uma outra vantagem é tornar o seu código extremamente reutilizável, deixando completamente claro para outros desenvolvedores o que é esperado da sua classe, função ou método, e dizendo também o que eles irão retornar. Isso além de elevar seu código a um outro nível, te permitirá entender com clareza códigos de outros desenvolvedores e tirar vantagens disso. Sem dúvidas o seu framework favorito faz isso dessa sintaxe e você talvez nem soubesse.
Conclusão
Nosso principal objetivo aqui é instigar a curiosidade e interesse de vocês sobre os temas abordados, e se você ficou curioso para saber mais sobre as Type Hints e Function Annotations e está pensando em como e porque usá-las, nossa meta foi alcançada!
E se você ficou interessado, você pode conhecer a nossa Cheat Sheet: “Type Hints” e mypy, traduzida para não deixar nenhuma dúvida na hora de usar essa nova sintaxe. E é claro, as documentações também são sempre um excelente ponto de partida.
É importante ressaltar que essas anotações são arbitrárias e opcionais, e por mais que a sua função tenha uma anotação que não é seguida como deveria, você vai ver alertas, mas isso não impedirá que o seu código rode normalmente como se não houvesse anotação alguma.
Deixe nos comentários qualquer dúvida, crítica ou sugestão, que vamos ficar super felizes em conversar com vocês!
Conheça um pouco mais sobre a nossa comunidade, BRAINS – Brazilian AI Networks
2 comentários