Controlando Símbolos e Combinando Submódulos

Intermediate

This tutorial is from open-source community. Access the source code

Introdução

Neste laboratório, você aprenderá conceitos importantes relacionados à organização de pacotes Python. Primeiro, você aprenderá como controlar os símbolos exportados usando __all__ em módulos Python. Essa habilidade é crucial para gerenciar o que é exposto de seus módulos.

Em segundo lugar, você entenderá como combinar submódulos para importações mais simples e dominará a técnica de divisão de módulos para uma melhor organização do código. Essas práticas aprimorarão a legibilidade e a capacidade de manutenção do seu código Python.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível intermediário com uma taxa de conclusão de 62%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Compreendendo a Complexidade da Importação de Pacotes

Ao começar a trabalhar com pacotes Python, você perceberá rapidamente que importar módulos pode se tornar bastante complicado e prolixo. Essa complexidade pode tornar seu código mais difícil de ler e escrever. Neste laboratório, analisaremos de perto essa questão e aprenderemos como simplificar o processo de importação.

Estrutura de Importação Atual

Primeiro, vamos abrir o terminal. O terminal é uma ferramenta poderosa que permite interagir com o sistema operacional do seu computador. Depois que o terminal estiver aberto, precisamos navegar até o diretório do projeto. O diretório do projeto é onde todos os arquivos relacionados ao nosso projeto Python são armazenados. Para fazer isso, usaremos o comando cd, que significa "change directory" (mudar diretório).

cd ~/project

Agora que estamos no diretório do projeto, vamos examinar a estrutura atual do pacote structly. Um pacote em Python é uma maneira de organizar módulos relacionados. Podemos usar o comando ls -la para listar todos os arquivos e diretórios dentro do pacote structly, incluindo arquivos ocultos.

ls -la structly

Você notará que existem vários módulos Python dentro do pacote structly. Esses módulos contêm funções e classes que podemos usar em nosso código. No entanto, se quisermos usar a funcionalidade desses módulos, atualmente precisamos usar instruções de importação longas. Por exemplo:

from structly.structure import Structure
from structly.reader import read_csv_as_instances
from structly.tableformat import create_formatter, print_table

Esses caminhos de importação longos podem ser problemáticos de escrever, especialmente se você precisar usá-los várias vezes em seu código. Eles também tornam seu código menos legível, o que pode ser um problema quando você está tentando entender ou depurar seu código. Neste laboratório, aprenderemos como organizar o pacote de uma forma que simplifique essas importações.

Vamos começar olhando o conteúdo do arquivo __init__.py do pacote. O arquivo __init__.py é um arquivo especial em pacotes Python. Ele é executado quando o pacote é importado e pode ser usado para inicializar o pacote e configurar quaisquer importações necessárias.

cat structly/__init__.py

Você provavelmente descobrirá que o arquivo __init__.py está vazio ou contém muito pouco código. Nos próximos passos, modificaremos este arquivo para simplificar nossas instruções de importação.

O Objetivo

Ao final deste laboratório, nosso objetivo é poder usar instruções de importação muito mais simples. Em vez dos longos caminhos de importação que vimos anteriormente, poderemos usar instruções como:

from structly import Structure, read_csv_as_instances, create_formatter, print_table

Ou mesmo:

from structly import *

Usar essas instruções de importação mais simples tornará nosso código mais limpo e fácil de trabalhar. Também nos economizará tempo e esforço ao escrever e manter nosso código.

Controlando Símbolos Exportados com __all__

Em Python, quando você usa a instrução from module import *, pode querer controlar quais símbolos (funções, classes, variáveis) são importados de um módulo. É aqui que a variável __all__ é útil. A instrução from module import * é uma maneira de importar todos os símbolos de um módulo para o namespace atual. No entanto, às vezes você não quer importar todos os símbolos, especialmente se houver muitos ou se alguns forem destinados a serem internos ao módulo. A variável __all__ permite que você especifique exatamente quais símbolos devem ser importados ao usar esta instrução.

O que é __all__?

A variável __all__ é uma lista de strings. Cada string nesta lista representa um símbolo (função, classe ou variável) que um módulo exporta quando alguém usa a instrução from module import *. Se a variável __all__ não estiver definida em um módulo, a instrução import * importará todos os símbolos que não começam com um sublinhado. Símbolos que começam com um sublinhado são tipicamente considerados privados ou internos ao módulo e não devem ser importados diretamente.

Modificando Cada Submódulo

Agora, vamos adicionar a variável __all__ a cada submódulo no pacote structly. Isso nos ajudará a controlar quais símbolos são exportados de cada submódulo quando alguém usa a instrução from module import *.

  1. Primeiro, vamos modificar structure.py:
touch ~/project/structly/structure.py

Este comando cria um novo arquivo chamado structure.py no diretório structly do seu projeto. Após criar o arquivo, precisamos adicionar a variável __all__. Adicione esta linha perto do topo do arquivo, logo após as instruções de importação:

__all__ = ['Structure']

Esta linha diz ao Python que, quando alguém usa from structure import *, apenas o símbolo Structure será importado. Salve o arquivo e saia do editor.

  1. Em seguida, vamos modificar reader.py:
touch ~/project/structly/reader.py

Este comando cria um novo arquivo chamado reader.py no diretório structly. Agora, procure no arquivo todas as funções que começam com read_csv_as_. Essas funções são as que queremos exportar. Em seguida, adicione uma lista __all__ com todos esses nomes de funções. Deve ser algo parecido com isto:

__all__ = ['read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns']

Observe que os nomes reais das funções podem variar dependendo do que você encontrar no arquivo. Certifique-se de incluir todas as funções read_csv_as_* que você encontrar. Salve o arquivo e saia do editor.

  1. Agora, vamos modificar tableformat.py:
touch ~/project/structly/tableformat.py

Este comando cria um novo arquivo chamado tableformat.py no diretório structly. Adicione esta linha perto do topo do arquivo:

__all__ = ['create_formatter', 'print_table']

Esta linha especifica que, quando alguém usa from tableformat import *, apenas os símbolos create_formatter e print_table serão importados. Salve o arquivo e saia do editor.

Importações Unificadas em __init__.py

Agora que cada módulo define o que exporta, podemos atualizar o arquivo __init__.py para importar todos esses símbolos. O arquivo __init__.py é um arquivo especial em pacotes Python. Ele é executado quando o pacote é importado e pode ser usado para inicializar o pacote e importar símbolos de submódulos.

touch ~/project/structly/__init__.py

Este comando cria um novo arquivo __init__.py no diretório structly. Mude o conteúdo do arquivo para:

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

Essas linhas importam todos os símbolos exportados dos submódulos structure, reader e tableformat. O ponto (.) antes dos nomes dos módulos indica que estas são importações relativas, o que significa que são importações de dentro do mesmo pacote. Salve o arquivo e saia do editor.

Testando Nossas Mudanças

Vamos criar um arquivo de teste simples para verificar se nossas alterações funcionam. Este arquivo de teste tentará importar os símbolos que especificamos nas variáveis __all__ e imprimirá uma mensagem de sucesso se as importações forem bem-sucedidas.

touch ~/project/test_structly.py

Este comando cria um novo arquivo chamado test_structly.py no diretório do projeto. Adicione este conteúdo ao arquivo:

## A simple test to verify our imports work correctly

from structly import Structure
from structly import read_csv_as_instances
from structly import create_formatter, print_table

print("Successfully imported all required symbols!")

Essas linhas tentam importar a classe Structure, a função read_csv_as_instances e as funções create_formatter e print_table do pacote structly. Se as importações forem bem-sucedidas, o programa imprimirá a mensagem "Successfully imported all required symbols!". Salve o arquivo e saia do editor. Agora vamos executar este teste:

cd ~/project
python test_structly.py

O comando cd ~/project altera o diretório de trabalho atual para o diretório do projeto. O comando python test_structly.py executa o script test_structly.py. Se tudo estiver funcionando corretamente, você deverá ver a mensagem "Successfully imported all required symbols!" impressa na tela.

Exportando Tudo do Pacote

Em Python, a organização de pacotes é crucial para gerenciar o código de forma eficaz. Agora, vamos levar a organização do nosso pacote um passo adiante. Definiremos quais símbolos devem ser exportados no nível do pacote. Exportar símbolos significa disponibilizar certas funções, classes ou variáveis para outras partes do seu código ou para outros desenvolvedores que possam usar seu pacote.

Adicionando __all__ ao Pacote

Ao trabalhar com pacotes Python, você pode querer controlar quais símbolos são acessíveis quando alguém usa a instrução from structly import *. É aqui que a lista __all__ é útil. Ao adicionar uma lista __all__ ao arquivo __init__.py do pacote, você pode controlar precisamente quais símbolos estão disponíveis quando alguém usa a instrução from structly import *.

Primeiro, vamos criar ou atualizar o arquivo __init__.py. Usaremos o comando touch para criar o arquivo, caso ele não exista.

touch ~/project/structly/__init__.py

Agora, abra o arquivo __init__.py e adicione uma lista __all__. Esta lista deve incluir todos os símbolos que queremos exportar. Os símbolos são agrupados com base em sua origem, como os módulos structure, reader e tableformat.

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

## Define what symbols are exported when using "from structly import *"
__all__ = ['Structure',  ## from structure
           'read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns',  ## from reader
           'create_formatter', 'print_table']  ## from tableformat

Após adicionar o código, salve o arquivo e saia do editor.

Compreendendo import *

O padrão from module import * geralmente não é recomendado na maioria dos códigos Python. Existem várias razões para isso:

  1. Pode poluir seu namespace com símbolos inesperados. Isso significa que você pode acabar com variáveis ou funções em seu namespace atual que você não esperava, o que pode levar a conflitos de nomes.
  2. Torna obscuro de onde vêm os símbolos específicos. Quando você usa import *, é difícil dizer de qual módulo um símbolo está vindo, o que pode tornar seu código mais difícil de entender e manter.
  3. Pode levar a problemas de sombreamento (shadowing). O sombreamento ocorre quando uma variável ou função local tem o mesmo nome que uma variável ou função de outro módulo, o que pode causar um comportamento inesperado.

No entanto, existem casos específicos em que o uso de import * é apropriado:

  • Para pacotes projetados para serem usados como um todo coeso. Se um pacote for destinado a ser usado como uma única unidade, o uso de import * pode facilitar o acesso a todos os símbolos necessários.
  • Quando um pacote define uma interface clara via __all__. Ao usar a lista __all__, você pode controlar quais símbolos são exportados, tornando mais seguro o uso de import *.
  • Para uso interativo, como em um REPL (Read-Eval-Print Loop) Python. Em um ambiente interativo, pode ser conveniente importar todos os símbolos de uma vez.

Testando com Import *

Para verificar se podemos importar todos os símbolos de uma vez, vamos criar outro arquivo de teste. Usaremos o comando touch para criar o arquivo.

touch ~/project/test_import_all.py

Agora, abra o arquivo test_import_all.py e adicione o seguinte conteúdo. Este código importa todos os símbolos do pacote structly e, em seguida, testa se alguns dos símbolos importantes estão disponíveis.

## Test importing everything at once

from structly import *

## Try using the imported symbols
print(f"Structure symbol is available: {Structure is not None}")
print(f"read_csv_as_instances symbol is available: {read_csv_as_instances is not None}")
print(f"create_formatter symbol is available: {create_formatter is not None}")
print(f"print_table symbol is available: {print_table is not None}")

print("All symbols successfully imported!")

Salve o arquivo e saia do editor. Agora, vamos executar o teste. Primeiro, navegue até o diretório do projeto usando o comando cd e, em seguida, execute o script Python.

cd ~/project
python test_import_all.py

Se tudo estiver configurado corretamente, você deverá ver a confirmação de que todos os símbolos foram importados com sucesso.

Divisão de Módulos para Melhor Organização do Código

À medida que seus projetos Python crescem, você pode descobrir que um único arquivo de módulo se torna bastante grande e contém vários componentes relacionados, mas distintos. Quando isso acontece, é uma boa prática dividir o módulo em um pacote com submódulos. Essa abordagem torna seu código mais organizado, mais fácil de manter e mais escalável.

Compreendendo a Estrutura Atual

O módulo tableformat.py é um bom exemplo de um módulo grande. Ele contém várias classes de formatadores, cada uma responsável por formatar dados de uma maneira diferente:

  • TableFormatter (classe base): Esta é a classe base para todas as outras classes de formatadores. Ele define a estrutura básica e os métodos que as outras classes herdarão e implementarão.
  • TextTableFormatter: Esta classe formata dados em texto simples.
  • CSVTableFormatter: Esta classe formata dados em formato CSV (Valores Separados por Vírgula).
  • HTMLTableFormatter: Esta classe formata dados em formato HTML (Linguagem de Marcação de Hipertexto).

Vamos reorganizar este módulo em uma estrutura de pacote com arquivos separados para cada tipo de formatador. Isso tornará o código mais modular e mais fácil de gerenciar.

Passo 1: Limpar Arquivos de Cache

Antes de começarmos a reorganizar o código, é uma boa ideia limpar quaisquer arquivos de cache Python. Esses arquivos são criados pelo Python para acelerar a execução do seu código, mas às vezes podem causar problemas ao fazer alterações no seu código.

cd ~/project/structly
rm -rf __pycache__

Nos comandos acima, cd ~/project/structly altera o diretório atual para o diretório structly em seu projeto. rm -rf __pycache__ exclui o diretório __pycache__ e todo o seu conteúdo. A opção -r significa recursivo, o que significa que ele excluirá todos os arquivos e subdiretórios dentro do diretório __pycache__. A opção -f significa forçar, o que significa que ele excluirá os arquivos sem pedir confirmação.

Passo 2: Criar a Nova Estrutura do Pacote

Agora, vamos criar uma nova estrutura de diretórios para nosso pacote. Criaremos um diretório chamado tableformat e um subdiretório chamado formats dentro dele.

mkdir -p tableformat/formats

O comando mkdir é usado para criar diretórios. A opção -p significa pais, o que significa que ele criará todos os diretórios pai necessários, caso eles não existam. Portanto, se o diretório tableformat não existir, ele será criado primeiro e, em seguida, o diretório formats será criado dentro dele.

Passo 3: Mover e Renomear o Arquivo Original

Em seguida, moveremos o arquivo original tableformat.py para a nova estrutura e o renomearemos para formatter.py.

mv tableformat.py tableformat/formatter.py

O comando mv é usado para mover ou renomear arquivos. Neste caso, estamos movendo o arquivo tableformat.py para o diretório tableformat e renomeando-o para formatter.py.

Passo 4: Dividir o Código em Arquivos Separados

Agora precisamos criar arquivos para cada formatador e mover o código relevante para eles.

1. Criar o arquivo do formatador base

touch tableformat/formatter.py

O comando touch é usado para criar um arquivo vazio. Neste caso, estamos criando um arquivo chamado formatter.py no diretório tableformat.

Manteremos a classe base TableFormatter e quaisquer funções utilitárias gerais, como print_table e create_formatter, neste arquivo. O arquivo deve ser parecido com:

## Base TableFormatter class and utility functions

__all__ = ['TableFormatter', 'print_table', 'create_formatter']

class TableFormatter:
    def headings(self, headers):
        '''
        Emit table headings.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        raise NotImplementedError()

def print_table(objects, columns, formatter):
    '''
    Make a nicely formatted table from a list of objects and attribute names.
    '''
    formatter.headings(columns)
    for obj in objects:
        rowdata = [getattr(obj, name) for name in columns]
        formatter.row(rowdata)

def create_formatter(fmt):
    '''
    Create an appropriate formatter given an output format name.
    '''
    if fmt == 'text':
        from .formats.text import TextTableFormatter
        return TextTableFormatter()
    elif fmt == 'csv':
        from .formats.csv import CSVTableFormatter
        return CSVTableFormatter()
    elif fmt == 'html':
        from .formats.html import HTMLTableFormatter
        return HTMLTableFormatter()
    else:
        raise ValueError(f'Unknown format {fmt}')

A variável __all__ é usada para especificar quais símbolos devem ser importados quando você usa from module import *. Neste caso, estamos especificando que apenas os símbolos TableFormatter, print_table e create_formatter devem ser importados.

A classe TableFormatter é a classe base para todas as outras classes de formatadores. Ele define dois métodos, headings e row, que devem ser implementados pelas subclasses.

A função print_table é uma função utilitária que recebe uma lista de objetos, uma lista de nomes de colunas e um objeto formatador e imprime os dados em uma tabela formatada.

A função create_formatter é uma função de fábrica que recebe um nome de formato como argumento e retorna um objeto formatador apropriado.

Salve e saia do arquivo após fazer essas alterações.

2. Criar o formatador de texto

touch tableformat/formats/text.py

Adicionaremos apenas a classe TextTableFormatter a este arquivo.

## Text formatter implementation

__all__ = ['TextTableFormatter']

from ..formatter import TableFormatter

class TextTableFormatter(TableFormatter):
    '''
    Emit a table in plain-text format
    '''
    def headings(self, headers):
        print(' '.join('%10s' % h for h in headers))
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        print(' '.join('%10s' % d for d in rowdata))

A variável __all__ especifica que apenas o símbolo TextTableFormatter deve ser importado quando você usa from module import *.

A instrução from ..formatter import TableFormatter importa a classe TableFormatter do arquivo formatter.py no diretório pai.

A classe TextTableFormatter herda da classe TableFormatter e implementa os métodos headings e row para formatar os dados em texto simples.

Salve e saia do arquivo após fazer essas alterações.

3. Criar o formatador CSV

touch tableformat/formats/csv.py

Adicionaremos apenas a classe CSVTableFormatter a este arquivo.

## CSV formatter implementation

__all__ = ['CSVTableFormatter']

from ..formatter import TableFormatter

class CSVTableFormatter(TableFormatter):
    '''
    Output data in CSV format.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(str(d) for d in rowdata))

Semelhante às etapas anteriores, estamos especificando a variável __all__, importando a classe TableFormatter e implementando os métodos headings e row para formatar os dados em formato CSV.

Salve e saia do arquivo após fazer essas alterações.

4. Criar o formatador HTML

touch tableformat/formats/html.py

Adicionaremos apenas a classe HTMLTableFormatter a este arquivo.

## HTML formatter implementation

__all__ = ['HTMLTableFormatter']

from ..formatter import TableFormatter

class HTMLTableFormatter(TableFormatter):
    '''
    Output data in HTML format.
    '''
    def headings(self, headers):
        print('<tr>', end='')
        for h in headers:
            print(f'<th>{h}</th>', end='')
        print('</tr>')

    def row(self, rowdata):
        print('<tr>', end='')
        for d in rowdata:
            print(f'<td>{d}</td>', end='')
        print('</tr>')

Novamente, estamos especificando a variável __all__, importando a classe TableFormatter e implementando os métodos headings e row para formatar os dados em formato HTML.

Salve e saia do arquivo após fazer essas alterações.

Passo 5: Criar Arquivos de Inicialização do Pacote

Em Python, os arquivos __init__.py são usados para marcar diretórios como pacotes Python. Precisamos criar arquivos __init__.py nos diretórios tableformat e formats.

1. Criar um para o pacote tableformat

touch tableformat/__init__.py

Adicione este conteúdo ao arquivo:

## Re-export the original symbols from tableformat.py
from .formatter import *

Esta instrução importa todos os símbolos do arquivo formatter.py e os disponibiliza quando você importa o pacote tableformat.

Salve e saia do arquivo após fazer essas alterações.

2. Criar um para o pacote formats

touch tableformat/formats/__init__.py

Você pode deixar este arquivo vazio ou adicionar uma docstring simples:

'''
Format implementations for different output formats.
'''

A docstring fornece uma breve descrição do que o pacote formats faz.

Salve e saia do arquivo após fazer essas alterações.

Passo 6: Testar a Nova Estrutura

Vamos criar um teste simples para verificar se nossas alterações funcionam corretamente.

cd ~/project
touch test_tableformat.py

Adicione este conteúdo ao arquivo test_tableformat.py:

## Test the tableformat package restructuring

from structly import *

## Create formatters of each type
text_fmt = create_formatter('text')
csv_fmt = create_formatter('csv')
html_fmt = create_formatter('html')

## Define some test data
class TestData:
    def __init__(self, name, value):
        self.name = name
        self.value = value

## Create a list of test objects
data = [
    TestData('apple', 10),
    TestData('banana', 20),
    TestData('cherry', 30)
]

## Test text formatter
print("\nText Format:")
print_table(data, ['name', 'value'], text_fmt)

## Test CSV formatter
print("\nCSV Format:")
print_table(data, ['name', 'value'], csv_fmt)

## Test HTML formatter
print("\nHTML Format:")
print_table(data, ['name', 'value'], html_fmt)

Este código de teste importa as funções e classes necessárias do pacote structly, cria formatadores de cada tipo, define alguns dados de teste e, em seguida, testa cada formatador imprimindo os dados no formato correspondente.

Salve e saia do arquivo após fazer essas alterações. Agora execute o teste:

python test_tableformat.py

Você deve ver os mesmos dados formatados de três maneiras diferentes (texto, CSV e HTML). Se você vir a saída esperada, significa que a reorganização do seu código foi bem-sucedida.

Resumo

Neste laboratório, você aprendeu várias técnicas cruciais de organização de pacotes Python. Primeiro, você dominou o uso da variável __all__ para definir explicitamente os símbolos exportados por um módulo. Segundo, você criou uma interface de pacote mais amigável ao usuário, reexportando símbolos de submódulos do pacote de nível superior.

Essas técnicas são essenciais para criar pacotes Python limpos, sustentáveis e fáceis de usar. Elas permitem que você controle a visão do usuário, simplifique o processo de importação e organize logicamente o código à medida que seu projeto se expande.