Pacotes em Python
LidoEsta aula explora a criação e organização de pacotes em Python, cobrindo o papel do arquivo __init__.py, a estrutura de subpacotes, o uso de imports relativos e a organização de projetos em pacotes. Ao final, você será capaz de estruturar projetos Python de forma modular e reutilizável.
Nesta aula, vamos mergulhar em um dos pilares da organização de código em Python: os pacotes. Se você já trabalhou com projetos maiores, sabe que manter tudo em um único arquivo torna o código difícil de manter, testar e reutilizar. Os pacotes resolvem isso permitindo agrupar módulos relacionados em uma hierarquia de diretórios, com um namespace próprio. Vamos aprender desde o básico, como criar um pacote simples, até tópicos avançados como subpacotes e imports relativos, fundamentais para construir projetos profissionais.
Entender pacotes é essencial para distribuir bibliotecas, trabalhar em equipe e aproveitar ao máximo o ecossistema Python. Nesta aula, você verá como o arquivo __init__.py transforma um diretório comum em um pacote, como organizar código em subpacotes, e como usar imports relativos para evitar conflitos e tornar o código mais legível. Ao final, discutiremos boas práticas de estrutura de projeto, que o ajudarão a planejar seus próprios projetos desde o início.
__init__.py
O arquivo __init__.py é o marcador que indica ao Python que um diretório deve ser tratado como um pacote. Sem ele, o diretório é apenas uma pasta comum, e você não conseguirá importar módulos de dentro dele usando a sintaxe de pacote. Em versões antigas do Python (anteriores à 3.3), a presença desse arquivo era obrigatória; a partir da versão 3.3, foram introduzidos os 'namespace packages' que permitem pacotes sem __init__.py, mas ainda é uma prática altamente recomendada incluir esse arquivo para que o pacote seja tratado como um pacote regular e permita inicialização personalizada.
O __init__.py pode ser um arquivo vazio, mas geralmente contém código de inicialização do pacote, como a importação de sub-módulos para expor uma interface pública mais limpa. Por exemplo, se você tem um pacote chamado meu_pacote com um módulo modulo.py, pode importar diretamente de meu_pacote se no __init__.py você fizer from . import modulo. Isso permite que os usuários do pacote façam from meu_pacote import modulo ou até from meu_pacote import alguma_funcao se você expor funções específicas. O __init__.py também é o lugar ideal para definir a variável __all__, que controla o que é importado quando se usa from pacote import *.
Vamos criar um exemplo prático. Suponha a seguinte estrutura:
meu_pacote/
__init__.py
modulo.py
No arquivo __init__.py:
# __init__.py
from .modulo import funcao_principal
__all__ = ["funcao_principal"]
E no modulo.py:
# modulo.py
def funcao_principal():
return "Olá do módulo!"
Agora, do lado de fora, você pode importar:
from meu_pacote import funcao_principal
print(funcao_principal()) # Saída: Olá do módulo!
Isso é muito conveniente, pois esconde a estrutura interna do pacote e oferece uma API limpa.
Subpacotes
Assim como você pode ter módulos dentro de um pacote, também pode ter pacotes dentro de pacotes, chamados subpacotes. Isso permite uma hierarquia mais profunda, organizando o código em categorias. Por exemplo, um projeto de processamento de imagens pode ter um pacote processamento com subpacotes filtros, transformacoes e utils. Cada subpacote é um diretório contendo seu próprio __init__.py.
Para criar um subpacote, basta criar um diretório dentro de um pacote e adicionar um __init__.py dentro dele. A importação de módulos de subpacotes segue a mesma sintaxe de pontos. Por exemplo, se você tem a estrutura:
pacote_principal/
__init__.py
subpacote/
__init__.py
modulo_secundario.py
Você pode importar o módulo de duas formas:
# Importação absoluta (recomendada)
from pacote_principal.subpacote import modulo_secundario
# ou
import pacote_principal.subpacote.modulo_secundario
E dentro do código, você pode se referir à função modulo_secundario.minha_funcao().
Subpacotes são especialmente úteis para projetos grandes, pois permitem separar claramente diferentes responsabilidades. Por exemplo, em um projeto web, você pode ter app/, app/modelos, app/controladores e app/visoes. Cada subpacote pode conter vários módulos. Isso melhora a legibilidade e a manutenção, pois cada parte do sistema fica isolada em seu próprio namespace.
Vamos ver um exemplo concreto. Crie a seguinte estrutura:
animais/
__init__.py
mamiferos/
__init__.py
cao.py
gato.py
aves/
__init__.py
passaro.py
No arquivo animais/mamiferos/cao.py:
def latir():
return "Au au!"
No arquivo animais/aves/passaro.py:
def cantar():
return "Piu piu!"
Agora, para usar essas funções:
from animais.mamiferos.cao import latir
from animais.aves.passaro import cantar
print(latir())
print(cantar())
Note que cada subpacote pode ter seu próprio __init__.py para expor uma interface específica, mas isso é opcional.
Imports relativos
Dentro de um pacote, você pode importar módulos usando imports absolutos (como from pacote.subpacote import modulo) ou imports relativos, que usam pontos para indicar a posição relativa do módulo atual. Imports relativos são úteis para evitar repetir o nome do pacote raiz e para facilitar a renomeação ou movimentação do pacote sem quebrar as importações internas. A sintaxe usa um ponto para o pacote atual, dois pontos para o pacote pai, etc.
Por exemplo, dentro do módulo animais/mamiferos/cao.py, se quiséssemos importar algo de gato.py (mesmo diretório), usaríamos from . import gato ou from .gato import funcao. Se quiséssemos importar algo do pacote pai (animais), usaríamos from .. import algum_modulo. Três pontos significam o avô, e assim por diante.
Vamos ilustrar com um exemplo. Considere a estrutura:
pacote/
__init__.py
modulo_a.py
subpacote/
__init__.py
modulo_b.py
Suponha que modulo_b.py queira importar uma função de modulo_a.py. Como modulo_b está em pacote.subpacote, o pai é pacote, então o import relativo seria:
# modulo_b.py
from ..modulo_a import funcao_a
Isso é mais robusto do que usar o import absoluto from pacote.modulo_a import funcao_a, pois se você renomear o pacote raiz de pacote para meu_pacote, o import relativo continuará funcionando sem alteração.
Um ponto importante: imports relativos só funcionam dentro de módulos que fazem parte de um pacote. Se você executar um módulo diretamente como script (por exemplo, python modulo_b.py), os imports relativos falharão porque o módulo é tratado como __main__ e não tem pacote pai. Portanto, é recomendado usar imports relativos apenas em módulos que serão importados, não executados diretamente.
Exemplo prático: dentro de modulo_b.py:
# modulo_b.py
from ..modulo_a import funcao_a
def funcao_b():
return funcao_a() + " modificado por b"
E no modulo_a.py:
# modulo_a.py
def funcao_a():
return "Resultado de A"
Se você estiver na raiz do projeto (onde está o pacote pacote), pode testar:
from pacote.subpacote.modulo_b import funcao_b
print(funcao_b()) # Saída: Resultado de A modificado por b
Estrutura de projeto
Uma boa estrutura de projeto é fundamental para a manutenção e escalabilidade. Ao usar pacotes, você pode organizar seu código de forma lógica. Uma estrutura comum para projetos Python é:
meu_projeto/
meu_pacote/
__init__.py
modulo1.py
modulo2.py
subpacote/
__init__.py
modulo3.py
tests/
test_modulo1.py
test_modulo2.py
setup.py
README.md
requirements.txt
O diretório raiz meu_projeto contém o pacote principal (meu_pacote), onde fica o código-fonte. Os testes ficam em um diretório separado tests. O arquivo setup.py (ou pyproject.toml) é usado para empacotar e distribuir o projeto. Arquivos como README.md e requirements.txt documentam e listam dependências.
Dentro do pacote, é comum usar subpacotes para separar diferentes funcionalidades. Por exemplo, em um projeto de análise de dados, você pode ter:
meu_projeto/
analise/
__init__.py
processamento/
__init__.py
limpeza.py
transformacao.py
visualizacao/
__init__.py
graficos.py
utils/
__init__.py
helpers.py
Essa separação ajuda na organização e evita que módulos fiquem muito grandes. Além disso, você pode usar o __init__.py de cada subpacote para expor as funções mais importantes, criando uma API pública consistente.
Outra prática recomendada é usar imports relativos dentro do pacote para evitar dependências do nome do pacote raiz. Isso torna o código mais portátil. Por exemplo, dentro de analise/processamento/limpeza.py, se precisar usar uma função de analise/utils/helpers.py, você faria:
from ..utils import helpers
Ou, se estiver no mesmo nível, from . import modulo.
Além disso, é importante pensar no que será exposto publicamente. Use o __all__ nos __init__.py para definir a interface pública. Por exemplo:
# analise/__init__.py
from .processamento import limpeza
from .visualizacao import graficos
__all__ = ["limpeza", "graficos"]
Isso permite que, ao importar o pacote, você tenha acesso direto a esses subpacotes, sem expor detalhes internos.
Boas práticas
Ao trabalhar com pacotes, algumas boas práticas ajudam a manter o código limpo e sustentável:
- Sempre inclua
__init__.pynos diretórios que deseja tratar como pacotes, mesmo que vazio. Isso evita ambiguidades com namespace packages e garante compatibilidade com ferramentas. - Use imports relativos dentro do pacote para reduzir acoplamento e facilitar a movimentação do código.
- Defina o
__all__nos__init__.pypara controlar a exportação pública e evitar expor módulos internos. - Mantenha os módulos pequenos e com responsabilidade única. Se um módulo ficar muito grande, considere dividi-lo em um subpacote.
- Estruture os testes em um diretório separado, refletindo a estrutura do pacote, para facilitar a localização dos testes.
- Documente a estrutura do projeto em um README, explicando o propósito de cada pacote e como importar.
Referências
- Documentação oficial: Módulos e Pacotes
- Python Reference: Imports Relativos
- Empacotando projetos Python (PyPA)
- Documentação sobre __main__ e execução de scripts
- Real Python: Módulos e Pacotes
- pytest: framework de testes (para estrutura de projeto)
Exercícios
- Exercício 1: Crie um pacote chamado
calculadoracom um módulooperacoes.pyque contenha funções para somar, subtrair, multiplicar e dividir. No__init__.py, exponha essas funções diretamente. Após criar, escreva um script que importe e use as funções. - Exercício 2: Dentro do pacote
calculadora, crie um subpacoteavancadocom um módulopotencia.pyque tenha a funçãopotencia(base, expoente). Use import relativo no__init__.pyprincipal para expor essa função também. - Exercício 3: No módulo
operacoes.py, adicione uma funçãomedia(lista)que calcule a média de uma lista de números. Em seguida, dentro do mesmo pacote, crie um móduloestatisticas.pyque importesomaremediadeoperacoesusando imports relativos. Faça com queestatisticas.pyexponha uma funçãodesvio_padrao(lista)que use essas funções. - Exercício 4: Explique por que imports relativos não funcionam quando um módulo é executado diretamente como script. Dê um exemplo de erro que ocorreria.
- Exercício 5: Estruture um projeto chamado
sistema_bibliotecacom pacotes paramodelos(definir classes Livro, Usuario),servicos(funções de empréstimo) eutils(funções de validação). Descreva a estrutura de diretórios e mostre como um módulo deservicosimportaria demodelosusando import relativo.
Estrutura:
calculadora/
__init__.py
operacoes.py
operacoes.py:
def somar(a, b):
return a + b
def subtrair(a, b):
return a - b
def multiplicar(a, b):
return a * b
def dividir(a, b):
if b == 0:
raise ValueError("Divisão por zero")
return a / b
__init__.py:
from .operacoes import somar, subtrair, multiplicar, dividir
Script de uso:
from calculadora import somar, dividir
print(somar(5, 3))
print(dividir(10, 2))
Estrutura:
calculadora/
__init__.py
operacoes.py
avancado/
__init__.py
potencia.py
avancado/potencia.py:
def potencia(base, expoente):
return base ** expoente
avancado/__init__.py:
from .potencia import potencia
__init__.py principal:
from .operacoes import somar, subtrair, multiplicar, dividir
from .avancado import potencia
Adicione em operacoes.py:
def media(lista):
if not lista:
raise ValueError("Lista vazia")
return somar(*lista) / len(lista)
Crie estatisticas.py:
from .operacoes import somar, media
import math
def desvio_padrao(lista):
if len(lista) < 2:
raise ValueError("Desvio padrão requer pelo menos 2 elementos")
med = media(lista)
soma_quadrados = somar(*[(x - med) ** 2 for x in lista])
return math.sqrt(soma_quadrados / (len(lista) - 1))
Quando você executa um arquivo Python diretamente (ex.: python modulo.py), o Python define a variável __name__ como "__main__" e não associa o arquivo a um pacote. Imports relativos dependem do __package__ para resolver o ponto de partida. Sem pacote, o Python não sabe o que . significa, então lança um ImportError: attempted relative import with no known parent package. Por exemplo, se você tentar executar python meu_pacote/modulo.py e o módulo contém from . import outro_modulo, você verá esse erro.
Estrutura:
sistema_biblioteca/
__init__.py
modelos/
__init__.py
livro.py
usuario.py
servicos/
__init__.py
emprestimo.py
utils/
__init__.py
validacao.py
Em servicos/emprestimo.py, para importar a classe Livro:
from ..modelos.livro import Livro
E para importar a função de validação:
from ..utils.validacao import validar_cpf