Qual é a Diferença Entre Módulo e Pacote em Python?

Um módulo é um arquivo com a extensão .py contendo um script Python. O pacote é a maneira de manter os nomes desses módulos separados, usando a “notação ponto”, ou seja, <pacote>.<módulo>. Essa notação é usada internamente pelo Python para separar os módulos em “espaços de nomes” diferentes, mas para o sistema operacional, o pacote é apenas o diretório que contém os arquivos .py de cada módulo.

Esse post explora a fundo todos os conceitos que você precisa saber para entender o que é um módulo e o que é um pacote em Python e qual é a diferença entre eles.

O que é Namespace em Python?

Um namespace em Python é apenas um conjunto de nomes de objetos com um escopo definido, o que permite ter dois objetos com nomes iguais em contextos diferentes.

Namespaces Python
Figura 1 – Escopo de Namespaces em Python

Para entender a diferença entre módulo e pacote em Python, é preciso entender primeiro o conceito desse “espaço de nomes”.

Em Python tudo é um objeto e cada um desses objetos pode ter um nome.

Assim, vou usar o nome de uma função como exemplo.

Uma função é declarada em um script Python gravado em um arquivo .py. Ou seja, a função existe dentro de um módulo.

Por isso, o nome da função faz parte do espaço de nomes (namespace) global do módulo.

Veja o exemplo a seguir. A função imprime() é declarada no módulo modulocomfuncao, salvo no arquivo modulocomfuncao.py.

Exemplo de Módulo com Função
Figura 2 – Exemplo de Módulo com Função

Para poder usar essa função, basta importar o módulo modulocomfuncao no seu código. Veja como importar módulos mais adiante nesse post.

A linguagem Python não suporta overloading de funções como em Java. Isso significa que, dentro de um módulo, o nome da função é igual ao Highlander: Só pode haver um!

Viu como o conceito de namespace é simples?

Existe um outro namespace, chamado enclosed, para funções declaradas dentro de funções. Para simplificar, não vou falar desse tipo nesse post.

Veja uma analogia ainda mais fácil:

Você conhece pessoas de outras famílias que têm o mesmo nome que você, mas você não pode ter um irmão ou irmã com o mesmo nome, porque o “namespace” da sua família é o mesmo.

E por que isso é importante para entender a diferença entre módulo e pacote em Python?

Porque o mesmo acontece com os nomes dos módulos.

Nesse caso, é ainda mais fácil de entender, já que a limitação é bem rígida. Dois arquivos no mesmo diretório não podem ter o mesmo nome.

Até a versão 3.3 do Python, um diretório com arquivos .py (módulos) precisava ter um arquivo chamado __init__.py para ser considerado um pacote.

Na versão atual essa restrição não existe mais.

Por isso, para ter módulos com o mesmo nome, é preciso usar pacotes diferentes. Isso significa salvar os arquivos .py dos módulos em diretórios diferentes.

Como Organizar Módulos em um Pacote Python?

Os pacotes não servem só para separar os espaços de nomes dos módulos. Eles também são usados para organizar o código de um projeto para distribuição.

Se você desenvolver um programa com muitos módulos, é melhor colocar os arquivos em diretórios diferentes, para criar uma separação lógica no seu pacote.

Por exemplo, se você estiver criando um pacote que faça a manipulação de arquivos de imagem, você poderia colocar os módulos que leem e gravam as imagens de cada formato em um diretório diferente.

editordeimagem/
    formato/
        bitmap.py
        jpeg.py
        png.py
        webp.py
    efeito/
        blur.py
        crop.py
        distort.pyCode language: plaintext (plaintext)

Dessa forma fica mais fácil identificar para que serve cada módulo.

Agora que você já entendeu como separar os módulos em pacotes, veja a seguir como importar os módulos que você vai precisar no seu programa.

Como Importar um Módulo em Python?

A maneira abaixo é a mais simples de importar um módulo em Python:

import <módulo [, módulo]>Code language: plaintext (plaintext)

Quando está aprendendo a programar, você cria alguns scripts simples usando só as funções embutidas do Python.

Mas se você estiver escrevendo um programa real, é quase certo que você vai precisar usar objetos de pacotes escritos por outras pessoas, como pandas, NumPy, Beautiful Soup e Matplotlib.

Além disso, você pode ter criado módulos para reutilizar em outros programas, como no exemplo do módulo editordeimagem, mostrado na seção anterior.

Para tornar esses módulos disponíveis no seu código, é preciso usar a declaração import, como você viu na definição acima.

Eu sugiro que você execute o código Python mostrado nesse post no Jupyter Lab para treinar.

O Jupyter Lab é uma ferramenta ótima para começar a programar e deve fazer parte da sua lista de ferramentas como programador.

Se você ainda não tem o Jupyter Lab instalado, dá uma olhada nesse post onde eu explico o passo-a-passo para a instalação.

As declarações import podem ser escritas em qualquer parte do código. No entanto, segundo o guia de estilo para código Python PEP-8, elas devem ser colocadas no início do arquivo, depois apenas da string de documentação (docstring), se ela existir.

Se você já leu outros posts aqui no blog, sabe que eu sempre defendo que você deve entender o que está fazendo, para fazer certo sempre e não precisar usar o método de tentativa e erro cada vez que for programar.

Por esse motivo, é importante você saber o que acontece quando você importa um módulo.

Como você viu na figura 1, cada módulo cria seu próprio namespace. Quando você importa o módulo, só está disponibilizando o nome desse módulo no contexto atual, não o seu conteúdo.

Para usar uma função do módulo, você ainda precisa usar o nome do módulo da maneira como ele foi importado, com a “notação ponto”.

Por exemplo:

>>> import numpy

>>> a = array([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'array' is not defined

>>> a = numpy.array([1, 2, 3])
>>> a
array([1, 2, 3])Code language: Python (python)

Usando o exemplo da estrutura do pacote editordeimagem da seção anterior, para importar o módulo de edição de imagens no formato JPEG, você usaria a declaração import abaixo.

import editordeimagem.formato.jpegCode language: Python (python)

Cada vez que você fizer referência a um objeto desse módulo no seu código, vai ter que repetir esse nome todo. Por exemplo:

a = editordeimagem.formato.jpeg.Imagem()Code language: Python (python)

Para não precisar reescrever tudo, uma opção é usar um alias, ou um apelido, para mudar o nome do módulo importado.

import editordeimagem.formato.jpeg as edjpg

#  Usando o módulo no código

a = edjpg.Imagem()Code language: Python (python)

Muito mais fácil, não é?

Mas cuidado. Se você fizer isso, só pode referenciar o módulo pelo alias daí em diante.

>>> import numpy as np

>>> a = numpy.array([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'numpy' is not defined

>>> a = np.array([1, 2, 3])
>>> a
array([1, 2, 3])Code language: Python (python)

Se você só quiser usar alguns objetos do módulo, é possível importar só esses objetos, usando a sintaxe abaixo.

from <módulo> import <objeto(s)>Code language: plaintext (plaintext)

Dessa forma, você importa só os nomes dos objetos para o namespace atual e não o nome do módulo.

Usando o exemplo do módulo modulocomfuncao da figura 2, para importar só a função imprime(), você escreveria:

>>> from modulocomfuncao import imprime

>>> imprime('Mensagem')
Módulo com função. Imprime MensagemCode language: Python (python)

Você pode até importar todo o conteúdo de um módulo, usando a declaração import abaixo.

from <módulo> import *Code language: plaintext (plaintext)

No entanto, eu recomendo que você não importe um módulo dessa maneira.

Por quê?

Bem, quando você importa * de um módulo, os objetos importados sobrepõem outros objetos com o mesmo nome no namespace atual. Veja um exemplo disso a seguir.

>>> def array(lista):
...     return sum(lista)
...
>>> a = array([1, 2])
>>> a
3
>>> type(a)
<class 'int'>
>>> from numpy import *
>>> a = array([1, 2])
>>> a
array([1, 2])
>>> type(a)
<class 'numpy.ndarray'>
Code language: Python (python)

Na linha 1 eu declarei uma função fictícia com o nome array, que recebe uma lista de inteiros e retorna a soma dos valores da lista. Perceba que o tipo de retorno dessa função é int, como mostra o resultado na linha 7.

Após importar todos os objetos do módulo numpy, usando import *, o nome array sobrepõe a função declarada na linha 1 e passa a representar um objeto do tipo numpy.ndarray.

Isso mostra como um import * pode introduzir bugs no seu código.

Portanto, importe só os módulos ou objetos que você precisa e evite sobrepor objetos por acidente com import *.

Qual é a Diferença entre Importação Absoluta e Importação Relativa de Módulos em Python?

A importação absoluta é feita escrevendo todo o caminho desde o primeiro nível do pacote, até o nome do módulo. A importação relativa é feita usando pontos “.” para especificar os níveis do pacote em relação ao módulo atual, de forma semelhante ao comando cd, usado para navegar por diretórios Unix.

Para ficar mais fácil de entender, reveja o exemplo do pacote editordeimagem que eu mostrei no início do post, com uma pequena alteração.

editordeimagem/
formato/
		bitmap.py
jpeg.py
		png.py
		webp.py
	efeito/
		blur.py
		crop.py
		distort.py
		filtro/
			vividcool.pyCode language: Shell Session (shell)

Imagine que o módulo distort tem uma função dentro dele chamada execute().

Se você quiser importar essa função e o módulo png de dentro do módulo jpeg, pode usar as importações absolutas abaixo.

#  Dentro de jpeg.py
from editordeimagem.efeito.distort import execute
import editordeimagem.formato.pngCode language: Python (python)

Nesse caso, todo o caminho do pacote foi usado, desde a sua raiz editordeimagem, até o nome do módulo distort e do formato png.

Se você trocar os pontos “.” por barras “/”, vai perceber que isso é somente uma estrutura de diretórios comum.

A importação absoluta é a forma recomendada no guia de estilo para código Python PEP-8.

O único problema com esse estilo de importação é que a declaração pode ficar muito grande, se o pacote tiver muitos níveis.

Imagine se eu resolvesse aplicar a convenção de nomes usada em Java para esse pacote, colocando o nome de domínio invertido antes do pacote.

Além disso, e se eu quisesse criar mais um módulo chamado vividcool, mas agora dentro do diretório efeito/filtro?

As importações absolutas ficariam assim:

#  Dentro de jpeg.py
from com.vaiprogramar.editordeimagem.efeito.filtro.vividcool import execute
import com.vaiprogramar.editordeimagem.formato.pngCode language: Python (python)

Enorme, não é?

Nesse caso, como você está importando a partir do módulo jpeg, pode usar a importação relativa, usando um ponto “.” para referenciar o diretório corrente, dois pontos “..” para o diretório no nível acima e três pontos “…” para subir mais um nível.

#  Dentro de jpeg.py
from ..efeito.filtro.vividcool import execute
import .pngCode language: Python (python)

Parece mais fácil, não é?

É assim que se importa um arquivo Python de outro diretório.

Só que esse estilo de importação relativa cobra um preço muito alto.

Se você mudar alguma coisa na estrutura de diretórios do projeto, todas as declarações import em todos os módulos precisam ser alteradas!

E, em um projeto grande, você está sempre refatorando alguma parte do código e acrescentando novos módulos.

Portanto, use a importação absoluta e evite ter que lidar com problemas que você nem precisa ter.

Como Funciona o Caminho de Pesquisa de Módulos em Python?

Quando você importa um módulo no seu código, a primeira coisa que o interpretador Python faz é pesquisar se o módulo já está no dicionário sys.modules. Esse dicionário serve como um cache para os módulos que já foram importados antes. Se o nome do módulo não estiver nesse cache, o interpretador pesquisa o nome na lista de módulos embutidos na biblioteca padrão e na lista sys.path.

Essa lista é definida por meio da variável de ambiente PYTHONPATH, de maneira parecida com a variável PATH, com a relação dos diretórios onde os arquivos dos módulos podem estar. A primeira entrada dessa lista é o diretório atual.

O conteúdo da lista sys.path depende do sistema operacional e de como o Python foi instalado na máquina. Veja a seguir um exemplo para a instalação usando a distribuição Anaconda no Windows.

>>> import sys
>>> sys.path
['', 'C:\\Users\\Guilherme\\Anaconda3\\python37.zip', 
'C:\\Users\\Guilherme\\Anaconda3\\DLLs', 
'C:\\Users\\Guilherme\\Anaconda3\\lib', 
'C:\\Users\\Guilherme\\Anaconda3', 
'C:\\Users\\Guilherme\\Anaconda3\\lib\\site-packages', 
'C:\\Users\\Guilherme\\Anaconda3\\lib\\site-packages\\win32', 
'C:\\Users\\Guilherme\\Anaconda3\\lib\\site-packages\\win32\\lib', 
'C:\\Users\\Guilherme\\Anaconda3\\lib\\site-packages\\Pythonwin']Code language: Python (python)

Se o nome do módulo que você está tentando importar não for encontrado em nenhum desses lugares, o interpretador lança um ModuleNotFoundError.

Portanto, para conseguir importar um módulo, coloque o arquivo .py correspondente em um desses diretórios, ou configure a variável de ambiente PYTHONPATH, ou ainda, inclua um item na lista sys.path em tempo de execução, apontando para o diretório onde está o seu módulo.

Conclusão

Nesse post você aprendeu a diferença entre módulos e pacotes em Python e viu como os namespaces são usados para separar os nomes dos objetos.

Agora você já pode aplicar essas boas práticas para organizar seus pacotes em diretórios e importar pacotes da biblioteca padrão Python e de terceiros, sabendo como o interpretador encontra esses pacotes e entendendo a diferença entre importação absoluta e relativa.

Ainda ficou com alguma dúvida? Escreva nos comentários e ajude a melhorar esse post!

Guilherme Brügger D Amato - Audiência Pública na Comissão Senado do Futuro

Guilherme Brügger D’Amato é servidor concursado de TI na Câmara dos Deputados, onde ocupou o cargo de Diretor de Informática entre 2015 e 2016. Com mais de 26 anos de experiência como programador e executivo de TI, já desenvolveu sites e sistemas usados por dezenas de milhões de pessoas. Conecte-se com ele no LinkedIn ou no Instagram.

Deixe um comentário