Python tipado? “Type Hints” e “Function Annotations”

Robô encaixando um cubo em um brinquedo shape sorter
Fonte: DepositPhotos
Entenda um pouco mais sobre as Function Annotations e Type Hints do Python e saiba como tirar vantages de uma linguagem tipada mesmo usando Python.

Python (pode ser estaticamente) tipado? “Type Hints” e “Function Annotations

Mão robótica encaixando um cubo em um brinquedo shape sorter simulando código Python.
Fonte: DepositPhotos

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

#NoBrains #NoGains 🧠

0 Shares:
2 comentários
Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Você também pode gostar