Dicas de como documentar suas funções em Python

Assim como outras profissões, desenvolvedores não trabalham sozinhos, eles cooperam com outros devs e com outros profissionais. Muitas empresas utilizam a metodologia do pull request onde outros desenvolvedores devem revisar o código e aprovar ou não as novas alterações. Você como revisor quer que o código do coleguinha seja fácil de ler, pois assim, poderá passar seu tempo trabalhando em suas próprias tarefas e você como criador do pull request quer que ele seja aprovado rapidamente para trabalhar com outra coisa.

Diante disso, não basta somente sua nova feature funcionar, seu trabalho deve ser legível e fácil de entender para seus colegas e para você, que pode ficar meses sem mexer nele. Veremos algumas dicas de como documentar e deixar seu código legível.

Nossas funções

Digamos que sua tarefa seja implementar duas funções muito conhecidas, a função de calcular o valor investido utilizando juros compostos e a de calcular os juros simples. As expressões abaixo definem como calcular:

Juros compostos: $$ M = C * (1 + i)^t $$

Juros simples: $$ M = C(1 + i * t) $$

Abaixo segue uma possível forma de implementar as duas funções:

# Montante com juros compostos:
m1 = lambda pa, r, t: pa * (1 + r)**t

# Montante com juros simples:
m2 = lambda pa, r, t: pa * (1 + r * t)

Apesar de ter deixado comentários, as funções em si não estão nem um pouco legíveis, se desconsiderarmos os comentários e olharmos para as funções, não saberemos de primeira o que elas significam, além disso se importarmos de outros módulos, teríamos que abrir a implementação para entender.

Melhorando a legibilidade

Funções simples como essas não precisariam de comentários se forem bem escritas. Esse é um caso em que é melhor termos mais linhas de código e um código fácil de entender do que escrever tudo em uma linha. Vamos reescrevê-las não utilizando funções anônimas, lambda.

def calculate_compound_interest_investment(pa, r, t):
    return pa * (1 + r)**t

def calculate_simple_interest_investment(pa, r, t):
    return pa * (1 + r * t)

Houve uma grande melhora, os nomes das funções já informam o que elas fazem, tirando a necessidade de comentários. Mas ainda é possível melhorar, os nomes dos parâmetros não são muito sugestivos. Meses após implementar as funções você pode não lembrar o que é cada parâmetro. Portanto, vamos dar nomes melhores.

def calculate_compound_interest_investment(principal_amount, rate, time):
    return principal_amount * (1 + rate)**time

def calculate_simple_interest_investment(principal_amount, rate, time):
    return principal_amount * (1 + rate * time)

Docstrings

Temos duas funções legíveis, mas ainda podemos documentá-las. Em Python é possível documentar qualquer função ou classe colocando uma string na primeira linha, são as famosas docstrings. Vale notar que deve ser uma string e não comentário, comentários são ignorados durante a interpretação do código.

def calculate_compound_interest_investment(principal_amount, rate, time):
    """
    Returns the compound interest an accrued amount that includes
    principal plus interest.

    :param principal_amount: float
    :param rate: float
    :param time: int

    :return float principal_amount * (1 + rate)**time:
    """
    return principal_amount * (1 + rate)**time


def calculate_simple_interest_investment(principal_amount, rate, time):
    """
    Returns the simple interest an accrued amount that includes
    principal plus interest.

    :param principal_amount: float
    :param rate: float
    :param time: int

    :return float principal_amount * (1 + rate * time):
    """
    return principal_amount * (1 + rate * time)

Quando usamos docstring, podemos usar ferramentas externas para gerar documentações automaticamente, além disso, em IDEs modernas, ao usar uma função, podemos ver a documentação sem ter a necessidade de abrir o código fonte.

IDE Docstring

Visualizando nossa documentação

As docstring ficam salvas no método mágico __doc__ das funções e classes que as implementam. Portanto, algumas formas de ver a documentação de nossa função é chamar a função print com o método __doc__ ou chamando a função help.

print(calculate_compound_interest_investment.__doc__)

help(calculate_compound_interest_investment)

Em ambos os casos teremos algo do tipo:

calculate_compound_interest_investment(principal_amount, rate, time)
    Returns the compound interest an accrued amount that includes
    principal plus interest.

    :param principal_amount: float
    :param rate: float
    :param time: int

    :return float principal_amount * (1 + rate)**time

O Python tem um módulo built in muito útil para visualizar e gerar documentações, o Pydoc. Com ele você consegue buscar módulos por palavras-chaves, gerar a documentação em um arquivo HTML, subir um servidor local com as documentações etc. Se quisermos salvar a documentação de nossas funções em um arquivo HTML, as quais estão em um arquivo chamado main.py, podemos simplesmente executar:

$ python -m pydoc -w main
wrote main.html

Ao abrir o arquivo, você terá uma página assim: Pydoc 1

O Pydoc é muito útil, simples de usar, e é built in. Mas existem outras ferramentas, até mais robustas, como o Sphinx, mas para o propósito desse artigo, o Pydoc já é o suficiente.

Anotações e type hints

Python é uma linguagem fortemente e dinâmicamente tipada. Com isso, ele infere automaticamente o tipo do objeto durante a execução, e por ser fortemente tipada, não aceita esse tipo de atribuição:

x = '6' + 4

A atribuição acima ocasionará um TypeError, enquanto em Javascript, o x teria o valor de ‘64’ e em PHP teria o valor de 10, ambas as linguagens têm tipagem fraca e resolvem esse problema de forma diferentes.

No Python 3 foi introduzido anotações de funções, o que possibilita adicionar qualquer tipo de expressões Python nas definições das funções. São completamente opcionais, não é usada pelo Python, apenas para bibliotecas de terceiros e IDEs. Um exemplo para ficar mais claro:

def multiplica(x: 'multiplicando', y: 'multiplicador') -> 'produto':
    return x * y

Isso torna possível documentar melhor as funções, descrevendo o que é o parâmetro, ou melhor, o tipo do parâmetro::

def multiplica(x: float, y: float) -> float:
    return x * y

Colocar os tipos dos parâmetros nas definições das funções as tornam melhor documentadas, pois a maioria das IDEs conseguem mostrar os tipos quando vamos chamar a função, além de detectar quando passamos parâmetros com tipos errados:

Type Hint IDE

É possível também utilizar checadores estáticos de tipos, que encontram inconsistências em nosso código, um deles é o Mypy, que é mantido pela própria comunidade Python, para instalar basta executar:

pip install mypy

E para executar a checagem, basta executar: mypy . e teremos a seguinte mensagem de erro no exemplo da multiplicação:

main.py:33: error: Argument 2 to "multiplica" has incompatible type "str"; expected "float"
Found 1 error in 1 file (checked 1 source file)

As anotações ficam armazenadas no método mágico __annotations__ do objeto, em formato de um dicionário:

{'x': <class 'float'>, 'y': <class 'float'>, 'return': <class 'float'>}

Esse tópico por si só tem conteúdo para um post, recomendo que veja a Live de Python do Eduardo Mendes sobre o assunto.

Vamos adicionar type hints às nossas funções que nos deixará ricos em breve:

def calculate_compound_interest_investment(
        principal_amount: float, rate: float, time: int
) -> float:
    """
    Returns the compound interest an accrued amount that includes
    principal plus interest.

    :param principal_amount: float
    :param rate: float
    :param time: int

    :return float principal_amount * (1 + rate)**time
    """
    return principal_amount * (1 + rate) ** time


def calculate_simple_interest_investment(principal_amount: float, rate: float, time: int) -> float:
    """
    Returns the simple interest an accrued amount that includes
    principal plus interest.

    :param principal_amount: float
    :param rate: float
    :param time: int

    :return float principal_amount * (1 + rate * time):
    """
    return principal_amount * (1 + rate * time)

Annotations e type hints são ótimas formas de documentar seu trabalho. Apesar delas não serem usadas pelo Python, há diversas ferramentas e IDEs que as usam e ajudam a diminuir pequenos bugs de tipagem.

Testes

Pode não ser muito intuitivo, mas testes ajudam a deixar seus programas mais documentados. Nossas duas funções são simples, mas para funções complexas pode não ser suficiente ler a definição da função, sua docstring e suas anotações.

Além de ajudar a detectar bugs, os testes nos ajudam a entender o comportamento do nosso código, de ver o que é esperado dele dado os parâmetros de entrada.

Criei alguns testes, com o Pytest, para as nossas funções, você pode acessar aqui. Como ficou com mais de 100 linhas preferi não colocar os testes nesse post. Verá que tentei colocar nomes descritivos, deixando explícito o que cada um está testando, documentando o que se espera da função.

Conclusão

É difícil manter uma documentação sempre atualizada. Mas o ganho que documentar o código trás costuma valer o esforço para funções complexas, pois o tempo tentando entender algo que não está documentado pode ser maior do que o tempo gasto para manter a documentação atualizada, o que pode ser inserido na cultura do time e cobrado no code review.

As anotações e os type hints apesar de não serem usados pelo Python, tem maior facilidade de estarem mais atualizadas do que as docstrings, há várias ferramentas que as usam para validar a tipagem correta e evitar alguns bugs. Sendo úteis em documentação e segurança do código.

Quando as funções se tornam complexas, docstrings e type hints podem não ser o suficiente para entender o comportamento das funções, podemos analisar os testes para entender o funcionamento delas.

O que achou desse post? Concorda com algo? Discorda? Deixe um comentário mostrando seu ponto de vista!

via GIPHY

Recomendações

Créditos, referências e complementos: