Módulos e Pacotes

Executando o código

Podemos digitar comandos em sessões interativas do python diretamente em uma sessão do terminal, e ler nele os outputs das linhas executadas. Vários editores e IDEs (Integrated Development Environment), como Pycharm e VSCode, oferecem acesso a um terminal onde o código pode ser executado diretamente sem abandonar o ambiente de desenvolvimento. O Jupyter Notebook também roda de modo interativo, exibindo em suas células o resultado de blocos de código.

# sessão interativa do python (no terminal)
$ python
  Python 3.12.0 (main, Oct  2 2023, 00:00:00) [GCC 13.2.1 20230918 (Red Hat 13.2.1-3)] on linux
  Type "help", "copyright", "credits" or "license" for more information.
  >>> lista = ["sogra","da","Casa"]
  >>> for i in lista[::-1]:
  ...     print(i, end = " ")
  Casa da sogra

Para programas mais extensos, e que devem ser executados várias vezes, torna-se mais prático gravar em um arquivo as linhas de código e executá-lo depois, em bloco. Esses arquivos são chamados de scripts e podem ser divididos em múltiplos arquivos, se isso for necessário para facilitar seu desenvolvimento e manutenção.

Módulos

Uma prática comum consiste em gravar arquivos separados com um conjunto de funções que podem ser usadas em outras partes do mesmo projeto ou em projetos diferentes, o que facilita a reutilização do código. Esses arquivos são os módulos do Python. Ele devem ser gravados com extensão .py e seu nome (sem a extensão) é o nome do módulo, usado na sua importação. Dentro de cada módulo seu nome está disponível na variável global __name__.

Por exemplo, gravamos um arquivo com o nome texto.py com duas funções. Uma delas remove as letras duplicadas de uma string (banana ↦ ban), e outra retorna strings com suas letras invertidas entre maiúscula e minúsculas (AzUlEjO ↦ aZuLeJo).

# arquivo texto.py
def remove_duplicadas(palavra):
    removida, foi = "", ""
    for i in palavra:
        if not i in foi:
            removida += i
        foi += i    
    return removida

def inverte_caso(palavra):
    t = [x.upper() if x.islower() else x.lower() for x in palavra]
    return "".join(t)

Para usar essas funções criamos agora um arquivo que importa texto.py, dando a ele o nome de usa_texto.py.

# usa_texto.py
# (1º bloco)    
import texto as tx
print(tx.remove_duplicadas("inconstitucionalissimamente"))
print(tx.inverte_caso("rUA DA bAHIA"))

Em seguida o executamos no prompt do terminal, estando todos na mesma pasta:

$ python usa_texto.py
  incostualme
  Rua da Bahia

Outras formas de importação para usa_texto.py estão listadas abaixo, todas com o mesmo resultado no output:

# (2º bloco)
  from texto import *
  print(remove_duplicadas("inconstitucionalissimamente"))
  print(inverte_caso("rUA DA bAHIA"))

# (3º bloco)
  from texto import remove_duplicadas, inverte_caso
  print(remove_duplicadas("inconstitucionalissimamente"))
  print(inverte_caso("rUA DA bAHIA"))

# (4º bloco)
  import texto
  remove = texto.remove_duplicadas
  inverte = texto.inverte_caso
  print(remove("inconstitucionalissimamente"))
  print(inverte("rUA DA bAHIA"))

No 2º bloco todos os objetos são importados. Essa prática é desaconselhada porque, em módulos grandes, pode aumentar a chance de conflitos de nomes (de existirem objetos com nomes iguais em mais de um módulo). A instrução de import apenas indica o caminho onde estão os objetos que serão usados. Ela não implica carregamento de objetos não usados para a memória. import * promove a importação de todos os nomes de objetos exceto aqueles que começam com _ (underscore ou sublinhado). import module as m cria um aliás (um apelido) para module e from module import objeto as obj faz o mesmo para um objeto dentro do módulo. No 4º bloco as funções receberam nomes, o mesmo que from texto import remove_duplicadas as remove, etc.

Além de funções, um módulo pode conter outras instruções destinadas a inicializar o módulo. Elas são executados na primeira vez que o nome do módulo é encontrado em uma importação ou quando o arquivo é executado como um script. Cada módulo tem seu próprio namespace (leia sobre o assunto) ou tabela de símbolos, que é usado por todas suas as funções. Essas variáveis ​​globais no módulo não conflituam com outras eventuais que apareçam com o mesmo nome em outras partes do código. Mesmo assim é uma boa prática se referir às variáveis e funções de um módulo com a notação nome_modulo.nome_item, onde item é um objeto qualquer dentro do módulo.

Também podemos importar módulos dentro de outros módulos. Nomes (que são referências aos objetos) dos módulos importados são juntados no mesmo namespace do módulo importador. É uma convenção, mas não obrigatório, listar todas as instruções de importação no início do módulo, o que facilita a leitura do código.

Executando um módulo

Módulos podem ser preparados para executar diretamente no terminal. Para ver isso gravamos o seguinte conteúdo em arquivo com o nome fatorial.py.

# fatorial.py
import sys
def fatorial(n):
    if n <= 1:
        return 1
    else:
        return n * fatorial(n-1)
      
if __name__ == "__main__":
    print(fatorial(int(sys.argv[1])))    

Se esse arquivo for executado diretamente no terminal, a variável __name__ assume o valor “__main__” e a condição final é satisfeita. sys.argv[1] traz para o código um parâmetro digitado no terminal, após o nome do módulo.

$ python fatorial.py 8
40320
$ python fatorial.py 10
3628800

!# shebang


Podemos executar diretamente um arquivo de script se ele contiver uma referência ao interpretador. Para isso acrescentamos a 1ª linha com #!/usr/bin/env python3. O sinal #! é denominado shebang e é destinado a indicar o caminho para o interpretador, no caso python 3 na pasta #!/usr/bin/env. Gravamos o arquivo letras.py, com a função mai_min_uscula() com o efeito de transformar uma string em outra com letras alternadamente maiúsculas e minúsculas (azulejo ↦ aZuLeJo).

#!/usr/bin/env python3

def mai_min_uscula(palavra):
    coleta = ""
    for i in range(len(palavra)):
        j = palavra[i]
        coleta += j.upper() if i%2==0 else j.lower()
    return coleta   

if __name__ == "__main__":
    import sys
    print(mai_min_uscula(sys.argv[1]))

Antes de tentar rodá-lo diretamente no terminal temos que tornar o arquivo executável. No linux:

# para tornar o arquivo executável    
$ chmod +x letras.py

# para executá-lo
$ ./letras.py "mais vale um pássaro na mão"
MaIs vAlE Um pÁsSaRo nA MãO

Além de permitir executar diretamente o arquivo, o shebang também é útil quando queremos que outras versões do python instaladas no computador sejam usadas.

# o comando
$ ./letras.py "texto"
# será executado como
usr/bin/env python3 letras.py "texto"


Naturalmente se o script for executado em outra máquina pode ser necessário ajustar o endereço em #!endereço para corresponder a uma instalação presente do python. Apenas para recordar, se o módulo for importado, e não executado diretamente, a variável __name__ tem outro valor e o bloco final é ignorado.

Localização de módulos

Quando importamos o módulo caminho/modulo_nome.py o interpretador busca por esse nome

  • primeiro no módulo interno, (built-in);
  • se não encontrar ele busca nos diretórios listados na variável sys.path que contém caminho (ou pasta local, ativa no momento, se caminho não for especificado);
  • nos diretórios estabelecidos em PYTHONPATH, uma variável de ambiente;
  • ou na pasta default estabelecida pela instalação de seu python.

É possível aterar dinamicamente, dentro do código, o conteúdo de sys.path de forma a procurar por módulos nos diretórios estabelecidos.

Python Bytecode

Python é uma linguagem interpretada e o interpretador é instalado junto com o próprio python. A sintaxe do código é a mesma para qualquer plataforma mas o interpretador é diferente para cada uma delas. Dessa forma pode-se escrever um único código que roda em qualquer plataforma. Também existem compiladores para transformar o arquivo (ou projeto) em um executável.

Na execução o interpretador gera um arquivo de bytecode (instruções de máquina de baixo nível) que, em seguida pode ser executada pela máquina virtual do python (python virtual machine). Esse bytecode fica em __pycache__, um subdiretório do diretório ativo na compilação. Apenas módulos carregados por imports são armazenados em __pycache__. Para obter todo o código como arquivo.pyc ele deve ser compilado com instruções explícitas para isso (como mostrado abaixo).

Na complição os seguintes arquivos são armazenados, com as extensões:

  • arquivo.py: O código fonte sendo executado.
  • arquivo.pyc: arquivo bytecode compilado. Todos os módulos importados são convertidos em *.pyc pelo interpretador. Esse arquivo pode ser carregado mais rapidamente que o módulo original.
  • arquivo.pyo: Um arquivo *.pyc é criado quando o parâmetro de otimização (-O) é usado.
  • arquivo.pyd: Arquivo dll do Windows.

Quando um arquivo é executado em um terminal as extensões .pyc tem prioridade para serem carregadas, a menos que o código fonte tenha sido alterado mais recentemente. Na compilação as chaves (opções) -O ou -OO podem ser usadas para diminuir o tamanho do módulo complilado:

  • A opção -O remove as instruções assert,
  • a opção -OO remove as instruções assert e as docstrings.

Ao fazer isso deve-se ter certeza de que tais instruções não serão mais necessárias. Módulos compilados são carregados em menor tempo mas não executam mais rápido. O módulo compileall pode ser usado para criar arquivos .pyc para todos os módulos de um diretório.

Módulos na biblioteca padrão e módulos instalados


Na instalação padrão do Python diversos módulos são instalados por default e podem ser importados diretamente em qualquer outro módulo. Alguns deles estão descritos no artigo Biblioteca Padrão, neste site. Alguns módulos são específicos da plataforma, como o winreg para o Windows, que permite diversas operações com o registro desse sistema.

Um exemplo é o módulo sys com diversas propriedades e métodos úteis.

import sys
sys.version
'3.8.8 (default, Apr 13 2021, 19:58:26) \n[GCC 7.3.0]'
sys.path
['/home/guilherme/.anaconda3/lib/python38.zip',
 '/home/guilherme/.anaconda3/lib/python3.8',
 '/home/guilherme/.anaconda3/lib/python3.8/lib-dynload',
 '',
 '/home/guilherme/.anaconda3/lib/python3.8/site-packages']
# acrescenta um diretório no path
sys.path.append('/home/guilherme/Projetos/Python/unittest')

A variável sys.path contém uma lista de strings, inicializada com a variável de ambiente PYTHONPATH. Ela pode ser modificada com
sys.path.append('novo/caminho')

Pacotes (Packages)

Um pacote (ou package) é uma coleção de módulos, dispostos de forma organizada para tornar mais simples a construção do código, sua utilização e reutilização. Essa estrutura facilita a localização e execução cada um de seus módulos. Pacotes são compreendidos como:

  • aqueles instalados de fontes externas usando ferramentas como pip ou pipenv. Frequentemente buscamos pacotes no Python Package Index, (Pypi);
  • aqueles construídos pelo desenvolvedor com seu próprio código. Pacotes são usados como formas de estruturar o projeto.

Os pacotes permitem que os namespaces (as tabelas de referência entre nomes e objetos) de módulos do python sejam estruturados usando a notação de ponto. Por exemplo modulo1.modulo2 é uma referência para o submódulo modulo2 contido em modulo1. Isso ajuda a impedir conflito de nomes em projetos grandes, principalmente quando módulos diferentes foram escritos por desenvolvedores diferentes.

Um diretório com um pacote deve necessariamente conter um arquivo __init__.py. O nome do pacote é mesmo do diretório base. É boa prática dar nomes que nos ajudem a lembrar de sua funcionalidade.

Arquivo __init__.py


O arquivo especial __init__.py é sempre executado quando o pacote é importado. Ele pode ser vazio ou pode conter código de inicialização do aplicativo ou dos módulos em seu diretório. Uma prática comum é a de importar os módulos necessários nessa inicialização. Quando um subpacote é importado, por exemplo com import meu_app.gerar_excel (em referência à figura), os arquivos de inicialização são executados na seguinte ordem:

  1. meu_app.__init__.py
  2. meu_app.gerar_excel.__init__.py

Por exemplo, vamos exibir a estrutura de um pacote de nome meu_app, cuja finalidade é ler e escrever dados numéricos em arquivos .txt ou .csv, e construir planilhas Excel com esses dados. Na figura mostramos a estrutura de diretórios, com nomes após sinais de #. Usuários do pacote podem importar os módulos:

# para ler os arquivos csv    
import meu_app.ler.ler_csv

# para gerar planilhas com dados importados
import meu_app.gerar_excel.gerar_xl
# ou
from  meu_app.gerar_excel import gerar_xl

# para importar todos os módulos de meu_app.escrever from meu_app.escrever import *

Se o objeto buscado não for encontrado um ImportError é lançado. No último exemplo, podemos limitar os módulos importados com * inserindo a variável especial __all__ em __inti__.py. Se meu_app.escrever.__init__.py contiver a linha: __all__=["escrever_csv"] apenas esse módulo será importado. Essa variável recebe uma lista dos módulos a serem importados: __all__=["modulo1", "modulo2", ...].

Quando os pacotes estão estruturados como o meu_app na figura, contendo subpacotes, podemos importar módulos de pacotes diferentes usando caminhos completos. Por exemplo, se para gerar uma planilha precisamos de ler_csv usamos from meu_app.ler import ler_csv dentro de gerar_xl.

Podemos ainda usar um atributo especial, __path__ contendo uma lista com todos os caminhos onde existem pacotes. Desta forma subpacotes que são partes de um pacote central podem estar distribuídos em diretórios quaisquer.

Função dir()

A função built-in dir() retorna uma lista de nomes definidos em um namespace. Sem argumentos ela retorna a lista, em ordem alfabética, de todos os nomes definidos na tabela de símbolos local. Abaixo dir() é usado logo após a inicialização do python, após a importação do módulo math e depois da criação de uma nova classe.

$ python
Python 3.8.8 (default, Apr 13 2021, 19:58:26) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']

# importando math
>>> import math
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'math']

# se definirmos uma classe ou definirmos uma variável
>>> class Nova():
...     pass
>>> x =1010
>>> dir()
['Nova', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'math', 'x']

Dessa forma a função é útil para mostrar o que foi inserido na tabela após uma importação ou qualquer outra ação do usuário. Se um um módulo for passado como argumento dir(modulo) lista os nomes dentro desse módulo.

>>> dir(math)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']

Classes, Módulos e Pacotes

No python um módulo pode conter diversas classes (diferente do Java ou C#), funções e definições de constantes. Módulos muito grandes, com muitas classes, podem ser divididos, principalmente se existirem objetivos diferentes em suas classes. Módulos não precisam ser parte de um pacote. Um pacote pode conter diversos módulos, com funcionalidade e objetivo semelhante.

Além de tornar o código mais estruturado e fácil de ser lido (protanto mais fácil de manter) a estrutura de classes, módulos e pacotes favorece a reutilização de código. Essa prática está em acordo com o princípio DRY (“Don’t Repeat Yourself” ou “Não se repita”). A abordagem DRY para programação se refere à agrupar partes repetidas do código em funções, classes, módulos ou pacotes (dependendo da complexidade) que podem ser chamadas em partes diversas do projeto.

Compilando o código

Usamos aqui o termo “compilar” significando “gerar um bytecode”.

Quando executamos um aplicativo ou módulo do python apenas os módulos importados são armazenados como bytecodes. Para forçar a compilação e armazenamento podemos usar py_compile e compileall.

py_compile(arquivo.py) Compila arquivo.py para bytecode
compileall.compile_dir(dir) Compila todos os arquivos no diretório para bytecode
compileall.compile_file(arquivo.py) Compila arquivo.py para bytecode
compileall.compile_path() Compila arquivos em sys.path para bytecode

Esses módulos são rodados no terminal do sistema operacional:

# para compilar um arquivo
$ python -m py_compile arquivo_compilar.py

# para compilar arquivos em um diretório
$ python -m compileall.compile_dir caminho_da_pasta

Com esses processos (ou durante a execução usual do python) os arquivos *.pyc são colocados na pasta “__pycache__” com o nome arquivo_compilar.cpython-39.pyc, onde cpython-xx indica que foram compliados sob cpython-xx, sendo xx a versão do cpython. Esses arquivos podem ser executados com:

$ cd __pycache__    
$ python arquivo_gerado_bytecode.cpyton-39.pyc

Você pode entregar apenas os arquivos *.pyc para um usuário final que deve ter o interpretador do python instalado em seu computador para executá-los.

Embora python seja uma linguagem interpretada existem compiladores desenvolvidos para transformar um aplicativo em um executável. Entre eles:

  • py2exe transforma programas do Python em pacotes que podem rodar no Windows sem a necessidade de ter uma instalação do Python.
  • Nuitka the Python Compiler, um compilador compatível com python2 e python3.
  • Pyinstaller, empacota todo um aplicativo python, com suas dependências, em um pacote único. Funciona com python3.6 ou superior.

PyInstaller é um empacotador que permite ao usuário final executar o aplicativo sem instalar um interpretador python ou qualquer módulo. Ele funciona corretamente com os principais pacotes, como numpy, PyQt, Django e wxPython. Ele deve ser executado na versão específica de plataforma a que é destinado o pacote final. O PyInstaller tem sido usado com sucesso no Windows, Mac OS X, GNU/Linux, Solaris, FreeBSD e OpenBSD.

Python Eggs e Wheels


Python Eggs: Dois formatos de distribuição de aplicativos do python são os arquivos *.egg e *.whl. Eggs são uma tecnologia mais antiga, recentemente substituída pelo formato wheels. Um arquivo egg é basicamente um arquivo zipado com instruções para a instalação de um pacote. Apesar de ter sido substituído ainda existem arquivos eggs para serem baixados e instalados. Ele faz uso do pacote SetupTools, que é a forma padrão original de lidar com pacotes do PyPI (e outras fontes) usando comando de linha. Você pode criar seu próprio egg para distribuí-lo, ou instalar pacotes baixados de terceiros. (Documentação sobre eggs).

Python wheels: O formato Wheel foi criado para substituir os python eggs. Ele trabalha junto com o pip e foi projetado para conter todos os arquivos para uma instalação de maneira próxima do formato em disco após a instalação. Semelhante a um *.egg, arquivo *.whl também é um arquivo zip com extensão renomeada. Se essa extensão for renomeada (*.whl ↦ *.zip), o arquivo pode ser aberto e analisado com qualquer aplicativo zip.

O nome de uma instalação wheel contém informações sobre quais as versões do python e da plataforma são suportadas, o que é usado pelos instaladores para fazer uma escolha correta da versão do pacote a ser instalado. Existe suporte para wheels em pip >= 1.4 e setuptools >= 0.8.

Um único arquivo .whl contém todos os módulos de seu projeto, inclusive aqueles importados por ele. Uma vez criado ele pode ser carregado para um ambiente na nuvem, como o Pypi, e dali instalado por qualquer usuário que queira usá-lo. Se o arquivo se chama arquivo.whl ele pode ser instalado com o comando pip install arquivo.whl.

Para usar wheels precisamos instalá-lo. Certifique-se de que você tem uma versão recente do pip fazendo o upgrade, e instale o wheel e setuptolls, lembrando que o ideal é fazer isso em um ambiente virtual (Leia sobe Ambientes Virtuais):

# upgrade do pip    
$ pip install --upgrade pip
# instalação do wheel e setuptools
$ pip install wheel
$ pip install setuptools

Para a criação de um arquivo .whl devemos proceder da seguinte forma:

  • Copie todos os módulos e pacotes de seu projeto em um diretório raiz. Vamos chamá-lo aqui de raiz. Lembramos, como visto acima, que todos os diretórios e subdiretórios devem possuir um arquivo __init__.py. (Não obrigatório no diretório raiz.)
  • Crie um arquivo com o nome setup.py no diretório raiz. Esse arquivo deve conter um mínimo de informações como: nome da distribuição, número da versão e lista de nomes de pacotes.

Um exemplo mínimo desse arquivo seria o seguinte:

from setuptools import setup, find_packages

setup(
    # nome da distribuição
    name = 'nome_da_distribuição', 
    # versão
    version='1.0', 
  
    # definição de nomes dos pacotes e módulos (uma das 3 abordagens)
 
    # 1. se existem poucos módulos no diretório raiz
    packages = ['']
  
    # 2. liste todos os nomes dos pacotes
    packages = ['pacote1', 'pacote2', ...]
  
    # 3. encontre automaticamente todos os pacotes
    packages = find_packages()
)
  • O nome da distribuição será exibido com python -m pip list ou conda list.
  • A versão pode ser incrementada, conforme o desenvolvedor.
  • A 1ª abordagem pode ser usada quando existem poucos módulos na raiz e nenhum subdiretório com pacotes. Os módulos podem ser importados diretamente sem o uso do nome da distribuição (como import modulo_1 para importar o arquivo import modulo_1.py.
  • Na 2ª abordagem todos os pacotes estão na lista e a importação é feita como from pacote1 import modulo1 ou import pacate1.modulo1 as m1.
  • Na 3ª abordagem a lista packages é preenchida automaticamente pelo método find_packages().

Depois, no terminal, navegue até o diretório raiz e execute setup

$ cd raiz
$ python setup.py bdist_wheel

Esse procedimento cria .whl (o arquivo wheel) em um subdiretório chamado dist de raiz. (Ou seja, cria raiz/dist/nome_completo_do_wheel.wheel). Para testar seu funcionamento você pode instalá-lo com

$ pip install nome_completo_do_wheel.whl

Wheels são uma boa maneira de criar um repositório local, com as dependências para seu projeto, que pode ser instalado rapidamente. A criação de vários repositórios wheel facilitam a alternância entre versões para fins de teste. Quando usado em conjunto com os ambientes virtuais essa tecnologia permite verificar como as versões atualizadas das dependências de seu projeto podem afetar seu desepenho sem precisar baixá-las várias vezes.

Bibliografia

todos acessados em março de 2020.