Construindo uma Biblioteca Python do Zero — Parte 1

Vinícius Martins
6 min readOct 12, 2023

--

Aprenda a estruturar seus arquivos Python em módulos usando __init__, decorators e docstrings e muito mais, para criar sua própria biblioteca Python elegante e fácil de usar.

Photo by AltumCode on Unsplash

Com certeza se você está aprendendo python como eu, usa diversas bibliotecas como pandas, sklearn, numpy e etc. Mas já parou pra pensar como os desenvolvedores dessas bibliotecas constroem e organizam seus códigos, exibem a documentação dentro do código e criam mensagens de erros personalizadas? E mais, como fazer com que seus projetos se pareçam com essas bibliotecas famosas?

Esse artigo tem esse objetivo, de dar uma introdução a como estruturar seus arquivos e diretorios para que funcionem de forma escalável e seja simples de utilizar.

Então vamos lá.

Base do Projeto

Como exemplo, vamos imaginar que estamos escrevendo alguns códigos para facilitar o cálculo de algumas fórmulas matemáticas conhecidas. Pra iniciar, vamos fazer o cálculo das medidas do triângulo retângulo usando o teorema de Pitágoras.

Este é o código para cálculo do teorema de Pitágoras:

import numpy as np

def Pitagoras(a, b, calc='hip') -> float:
if calc == 'hip':
c = np.sqrt((a**2) + (b**2))
elif calc == 'cat':
c = np.sqrt((max(a, b)**2) - (min(a, b)**2))
return c

Se não lembra sobre o que é o teorema de Pitágoras, pode dar uma olhada aqui. Mas não se preocupe, vamos ao que realmente importa neste artigo, começando pela documentação.

Inserindo DocStrings

Perceba que, mesmo que o código acima seja “simples”, é difícil saber o que é cada variável (a, b e calc) e o que elas aceitam de parâmetros. Por isso, sempre é uma boa prática documentar o código, descrevendo qual o objetivo da função e o que são as variáveis. Para isso podemos usar as docstrings.

Segundo a PEP 257, que trata especificamente das convenções de docstrings para o python, “Uma docstring é uma string literal que ocorre como a primeira instrução em uma definição de módulo, função, classe ou método. Tal docstring torna-se o __doc__atributo especial desse objeto.”

Traduzindo, é uma string que você insere no início do seu código, e ela será disponibilizada quando é chamada a ajuda para função.

Para ver na prática, vou inserir a documentação do código como Docstrings. A minha documentação vai ser multilinha, por isso, uso aspas triplas, para iniciar e terminar a string.

Fica dessa forma:

import numpy as np

def Pitagoras(a, b, calc='hip') -> float:
#Docstrings
'''
Esta função tem como objetivo automatizar o cálculo de pitagoras,
onde tentamos achar o valor de um dos lados de um triângulo retângulo.
Parametros:
-----------
* ``a`` = int or float
Valor conhecido de um dos lados do triângulo retângulo,
podendo ser um cateto ou a hiponetusa.
* ``b`` = int or float
Valor conhecido do segundo lado do triângulo retângulo,
podendo ser um cateto ou a hiponetusa.
* ``calc`` = 'hip', 'cat', default='hip'
Valor que deseja encontrar sendo:
- hip = hipotenusa
- cat = cateto
'''
if calc == 'hip':
c = np.sqrt((a**2) + (b**2))
elif calc == 'cat':
c = np.sqrt((max(a, b)**2) - (min(a, b)**2))
return c

Pronto, já temos a nossa Docstrings. Agora, quando chamamos a função, irá aparecer a ajuda com a nossa documentação. Se usa o jupyter notebook, pressione SHIFT + TAB para visualizar a ajuda.

Pode confessar, se você ainda não conhecia, pensava que inserir essas Docstring fossem bem mais complicadas né. Mas agora vamos ao próximo tópico.

Estruturando o Diretório de Arquivos

Por enquanto, temos apenas uma função no nosso arquivo, mas ainda não funciona como as bibliotecas que usamos. Pense, depois que instalamos uma biblioteca, qual a primeira coisa que precisamos fazer para utiliza-la?

Se você pensou em importar a biblioteca, você pensou correto.

A ideia é que, ao invés de termos todo aquele código no nosso arquivo main.py, consigamos ter apenas o seguinte:

from mathematics import Pitagoras

result = Pitagoras(3,4)
print(result)

Onde mathematics seria a nossa biblioteca personalizada e Pitagoras é a nossa função.

Para conseguirmos isso, vamos precisar estruturar o diretório da seguinte forma:

.
├── mathematics
│ └── geometric
│ └── pitagoras.py
└── main.py

Sendo:

  • pitagoras.py é o arquivo que conterá a nossa função.
  • main.py é o arquivo que conterá o código demonstrado acima, importando a função.

Perceba que ao realizar a estruturação do diretório, se tentarmos rodar o arquivo main.py, ele dará o seguinte erro:

AttributeError: module 'mathematics' has no attribute 'Pitagoras'

Isso porque, da forma que construímos até agora, precisaríamos de especificar, no momento da importação, o arquivo em que se encontra a função, desta forma:

from mathematics.geometric.pitagoras import Pitagoras

result = Pitagoras(3,4)
print(result)

Mas ai nos deparamos com um problema. Perceba que estamos criando um módulo chamado geometric na nossa biblioteca. Este poderá conter várias funções, uma em cada arquivo e sempre que quisermos importar uma função diferente, vamos precisar saber qual o nome do arquivo que a função está localizada. Pense no pandas, por exemplo, existem centenas de arquivos, cada um com funções diferentes. Daí, imagina ter que saber a localização exata de cada função para poder importa-la individualmente. Nada prático né?

Para resolvermos esse problema, precisamos trabalhar com um tipo de arquivo especial.

Inserindo Arquivos __init__.py

Para ter uma explicação mais detalhada do que vamos ver aqui, você pode dar uma olhada nesta documentação do python.

Em síntese, o arquivo __init__.py faz com que o python reconheça que as pastas que contem os arquivos .py sejam transformadas em pacotes, habilitando a importação sem ter que especificar o nome do arquivo na hora da importação do seu projeto.

Devemos inserir o arquivo __init__.py dentro da pasta que queremos transformar em pacote.

Vamos ver dois exemplos:

Exemplo 1: Colocando o arquivo dentro da pasta geometric, a estrutura fica desta forma:

.
├── mathematics
│ └── geometric
| ├── __init__.py
│ └── _pitagoras.py
└── main.py

Perceba também que coloquei um underline antes do arquivo pitagoras (_pitagoras.py). Esta é uma convenção indicando que este arquivo não deve ser acessado diretamente e é uma boa prática adicioná-lo.

Dentro do arquivo, escrevemos o seguinte código:

#__init__.py
from ._pitagoras import Pitagoras

__all__ = [
'Pitagoras'
]

Nele, primeiro importamos a nossa função e em seguida, usamos o __all__ para disponibilizar a função para ser importada. Somente poderão ser importadas as funções que estiverem listadas nessa lista __all__. Isso traz outro benefício, onde é possível controlar o que deve estar disponível para ser usado ou não.

Por fim, no arquivo main.py conseguimos importar a função diretamente e irá funcionar normal.

from mathematics.geometric import Pitagoras

result = Pitagoras(3,4)
print(result)

Perceba que, como colocamos o __init__.py dentro da pasta geometric, precisamos especificar a pasta na importação. Porém, podemos suprimir ainda mais a importação como no exemplo 2.

Exemplo 2: Colocando o arquivo dentro da pasta mathematics, a estrutura fica desta forma:

.
├── mathematics
│ ├── __init__.py
│ └── geometric
│ └── _pitagoras.py
└── main.py

Assim como no exemplo anterior, vamos inserir o código no arquivo, mas desta vez, incluindo a pasta geometric no caminho desta forma:

from .geometric._pitagoras import Pitagoras

__all__ = [
'Pitagoras'
]

Fazendo desta forma, é possível importar a função da seguinte maneira:

from mathematics import Pitagoras

result = Pitagoras(3,4)
print(result)

Não existe a melhor forma, isso dependerá da estrutura e complexidade do projeto. Utilize de forma que deixará a usabilidade de sua biblioteca o mais simples possível.

Na segunda parte, vamos ver como colocar validações na nossa função e mensagens de erro personalizadas.

Clique aqui para ir para a parte 2.

O repostório com o código pode ser visto neste link.

--

--

Vinícius Martins

Profissional em Análise de Dados e Negócios. Especializando em Engenharia de dados, Ciência de Dados e Inteligência Artificial.