Importações de Módulos Circulares e Dinâmicas

Beginner

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

Introdução

Neste laboratório, você aprenderá sobre dois conceitos cruciais relacionados à importação em Python. As importações de módulos em Python podem, por vezes, resultar em dependências complexas, levando a erros ou estruturas de código ineficientes. As importações circulares (circular imports), onde dois ou mais módulos se importam mutuamente, criam um ciclo de dependência que pode causar problemas se não for gerenciado adequadamente.

Você também explorará as importações dinâmicas, que permitem que os módulos sejam carregados em tempo de execução, em vez de no início do programa. Isso proporciona flexibilidade e ajuda a evitar problemas relacionados à importação. Os objetivos deste laboratório são entender os problemas de importação circular, implementar soluções para evitá-los e aprender a usar importações dinâmicas de módulos de forma eficaz.

Compreendendo o Problema de Importação

Vamos começar entendendo o que são as importações de módulos. Em Python, quando você deseja usar funções, classes ou variáveis de outro arquivo (módulo), você usa a instrução import. No entanto, a forma como você estrutura suas importações pode levar a vários problemas.

Agora, vamos examinar um exemplo de uma estrutura de módulo problemática. O código em tableformat/formatter.py tem importações espalhadas por todo o arquivo. Isso pode não parecer um grande problema a princípio, mas cria problemas de manutenção e dependência.

Primeiro, abra o explorador de arquivos do WebIDE e navegue até o diretório structly. Executaremos alguns comandos para entender a estrutura atual do projeto. O comando cd é usado para alterar o diretório de trabalho atual, e o comando ls -la lista todos os arquivos e diretórios no diretório atual, incluindo os ocultos.

cd ~/project/structly
ls -la

Isso mostrará os arquivos no diretório do projeto. Agora, vamos olhar para um dos arquivos problemáticos usando o comando cat, que exibe o conteúdo de um arquivo.

cat tableformat/formatter.py

Você deve ver um código semelhante ao seguinte:

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

def create_formatter(name, column_formats=None, upper_headers=False):
    if name == 'text':
        formatter_cls = TextTableFormatter
    elif name == 'csv':
        formatter_cls = CSVTableFormatter
    elif name == 'html':
        formatter_cls = HTMLTableFormatter
    else:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Observe a colocação das instruções de importação no meio do arquivo. Isso é problemático por várias razões:

  1. Torna o código mais difícil de ler e manter. Quando você está olhando para um arquivo, espera ver todas as importações no início para que possa entender rapidamente de quais módulos externos o arquivo depende.
  2. Pode levar a problemas de importação circular. As importações circulares acontecem quando dois ou mais módulos dependem um do outro, o que pode causar erros e fazer com que seu código se comporte de forma inesperada.
  3. Quebra a convenção Python de colocar todas as importações no topo de um arquivo. Seguir as convenções torna seu código mais legível e mais fácil de entender para outros desenvolvedores.

Nos passos seguintes, exploraremos esses problemas com mais detalhes e aprenderemos como resolvê-los.

Explorando as Importações Circulares

Uma importação circular é uma situação em que dois ou mais módulos dependem um do outro. Especificamente, quando o módulo A importa o módulo B, e o módulo B também importa o módulo A, direta ou indiretamente. Isso cria um ciclo de dependência que o sistema de importação do Python não consegue resolver corretamente. Em termos mais simples, o Python fica preso em um loop tentando descobrir qual módulo importar primeiro, e isso pode levar a erros em seu programa.

Vamos experimentar com nosso código para ver como as importações circulares podem causar problemas.

Primeiro, executaremos o programa de estoque para verificar se ele funciona com a estrutura atual. Esta etapa nos ajuda a estabelecer uma linha de base e ver o programa funcionando como esperado antes de fazermos quaisquer alterações.

cd ~/project/structly
python3 stock.py

O programa deve ser executado corretamente e exibir os dados de estoque em uma tabela formatada. Se isso acontecer, significa que a estrutura de código atual está funcionando bem, sem problemas de importação circular.

Agora, vamos modificar o arquivo formatter.py. Normalmente, é uma boa prática mover as importações para o topo de um arquivo. Isso torna o código mais organizado e mais fácil de entender rapidamente.

cd ~/project/structly

Abra tableformat/formatter.py no WebIDE. Moveremos as seguintes importações para o topo do arquivo, logo após as importações existentes. Essas importações são para diferentes formatadores de tabela, como texto, CSV e HTML.

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Então, o início do arquivo agora deve ser assim:

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

Salve o arquivo e tente executar o programa de estoque novamente.

python3 stock.py

Você deve ver uma mensagem de erro sobre TableFormatter não estar definido. Este é um sinal claro de um problema de importação circular.

O problema ocorre por causa da seguinte cadeia de eventos:

  1. formatter.py tenta importar TextTableFormatter de formats/text.py.
  2. formats/text.py importa TableFormatter de formatter.py.
  3. Quando o Python tenta resolver essas importações, ele fica preso em um loop porque não consegue decidir qual módulo importar totalmente primeiro.

Vamos reverter nossas alterações para fazer o programa funcionar novamente. Edite tableformat/formatter.py e mova as importações de volta para onde estavam originalmente (após a definição da classe TableFormatter).

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Execute o programa novamente para confirmar que está funcionando.

python3 stock.py

Isso demonstra que, embora ter importações no meio do arquivo não seja a melhor prática em termos de organização do código, isso foi feito para evitar um problema de importação circular. Nos próximos passos, exploraremos soluções melhores.

Implementando o Registro de Subclasses

Em programação, as importações circulares podem ser um problema complicado. Em vez de importar diretamente as classes de formatador, podemos usar um padrão de registro. Nesse padrão, as subclasses se registram com sua classe pai. Esta é uma maneira comum e eficaz de evitar importações circulares.

Primeiro, vamos entender como podemos descobrir o nome do módulo de uma classe. O nome do módulo é importante porque o usaremos em nosso padrão de registro. Para fazer isso, executaremos um comando Python no terminal.

cd ~/project/structly
python3 -c "from structly.tableformat.formats.text import TextTableFormatter; print(TextTableFormatter.__module__); print(TextTableFormatter.__module__.split('.')[-1])"

Quando você executa este comando, verá uma saída como esta:

structly.tableformat.formats.text
text

Esta saída mostra que podemos extrair o nome do módulo da própria classe. Usaremos este nome de módulo mais tarde para registrar as subclasses.

Agora, vamos modificar a classe TableFormatter no arquivo tableformat/formatter.py para adicionar um mecanismo de registro. Abra este arquivo no WebIDE. Adicionaremos algum código à classe TableFormatter. Este código nos ajudará a registrar as subclasses automaticamente.

class TableFormatter(ABC):
    _formats = { }  ## Dicionário para armazenar formatadores registrados

    @classmethod
    def __init_subclass__(cls):
        name = cls.__module__.split('.')[-1]
        TableFormatter._formats[name] = cls

    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

O método __init_subclass__ é um método especial em Python. Ele é chamado sempre que uma subclasse de TableFormatter é criada. Neste método, extraímos o nome do módulo da subclasse e o usamos como uma chave para registrar a subclasse no dicionário _formats.

Em seguida, precisamos modificar a função create_formatter para usar o dicionário de registro. Esta função é responsável por criar o formatador apropriado com base no nome fornecido.

def create_formatter(name, column_formats=None, upper_headers=False):
    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

Depois de fazer essas alterações, salve o arquivo. Em seguida, vamos testar se o programa ainda funciona. Executaremos o script stock.py.

python3 stock.py

Se o programa for executado corretamente, significa que nossas alterações não quebraram nada. Agora, vamos dar uma olhada no conteúdo do dicionário _formats para ver como o registro funciona.

python3 -c "from structly.tableformat.formatter import TableFormatter; print(TableFormatter._formats)"

Você deve ver uma saída como esta:

{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>, 'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>, 'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}

Esta saída confirma que nossas subclasses estão sendo registradas corretamente no dicionário _formats. No entanto, ainda temos algumas importações no meio do arquivo. Na próxima etapa, corrigiremos esse problema usando importações dinâmicas.

Usando Importações Dinâmicas

Em programação, as importações são usadas para trazer código de outros módulos para que possamos usar sua funcionalidade. No entanto, às vezes, ter importações no meio de um arquivo pode tornar o código um pouco confuso e difícil de entender. Nesta parte, aprenderemos como usar importações dinâmicas para resolver esse problema. As importações dinâmicas são um recurso poderoso que nos permite carregar módulos em tempo de execução, o que significa que só carregamos um módulo quando realmente precisamos dele.

Primeiro, precisamos remover as instruções de importação que estão atualmente colocadas após a classe TableFormatter. Essas importações são importações estáticas, que são carregadas quando o programa é iniciado. Para fazer isso, abra o arquivo tableformat/formatter.py no WebIDE. Depois de abrir o arquivo, encontre e exclua as seguintes linhas:

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

Se você tentar executar o programa agora, executando o seguinte comando no terminal:

python3 stock.py

O programa falhará. A razão é que os formatadores não serão registrados no dicionário _formats. Você verá uma mensagem de erro sobre um formato desconhecido. Isso ocorre porque o programa não consegue encontrar as classes de formatador de que precisa para funcionar corretamente.

Para corrigir esse problema, modificaremos a função create_formatter. O objetivo é importar dinamicamente o módulo necessário quando ele for necessário. Atualize a função conforme mostrado abaixo:

def create_formatter(name, column_formats=None, upper_headers=False):
    if name not in TableFormatter._formats:
        __import__(f'{__package__}.formats.{name}')

    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

A linha mais importante nesta função é:

__import__(f'{__package__}.formats.{name}')

Esta linha importa dinamicamente o módulo com base no nome do formato. Quando o módulo é importado, sua subclasse de TableFormatter se registra automaticamente. Isso é graças ao método __init_subclass__ que adicionamos anteriormente. Este método é um método especial do Python que é chamado quando uma subclasse é criada e, em nosso caso, é usado para registrar a classe do formatador.

Depois de fazer essas alterações, salve o arquivo. Em seguida, execute o programa novamente usando o seguinte comando:

python3 stock.py

O programa agora deve funcionar corretamente, mesmo que tenhamos removido as importações estáticas. Para verificar se a importação dinâmica está funcionando conforme o esperado, limparemos o dicionário _formats e, em seguida, chamaremos a função create_formatter. Execute o seguinte comando no terminal:

python3 -c "from structly.tableformat.formatter import TableFormatter, create_formatter; TableFormatter._formats.clear(); print('Before:', TableFormatter._formats); create_formatter('text'); print('After:', TableFormatter._formats)"

Você deve ver uma saída semelhante a esta:

Before: {}
After: {'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>}

Esta saída confirma que a importação dinâmica está carregando o módulo e registrando a classe do formatador quando necessário.

Ao usar importações dinâmicas e registro de classe, criamos uma estrutura de código mais limpa e fácil de manter. Aqui estão os benefícios:

  1. Todas as importações agora estão no topo do arquivo, o que segue as convenções do Python. Isso torna o código mais fácil de ler e entender.
  2. Eliminamos as importações circulares. As importações circulares podem causar problemas em um programa, como loops infinitos ou erros difíceis de depurar.
  3. O código é mais flexível. Agora, podemos adicionar novos formatadores sem modificar a função create_formatter. Isso é muito útil em um cenário do mundo real, onde novos recursos podem ser adicionados ao longo do tempo.

Este padrão de uso de importações dinâmicas e registro de classe é comumente usado em sistemas de plug-in e frameworks. Nesses sistemas, os componentes precisam ser carregados dinamicamente com base nas necessidades do usuário ou nos requisitos do programa.

Resumo

Neste laboratório, você aprendeu sobre conceitos e técnicas cruciais de importação de módulos Python. Primeiro, você explorou as importações circulares, entendendo como as dependências circulares entre módulos podem levar a problemas e por que o tratamento cuidadoso é necessário para evitá-las. Em segundo lugar, você implementou o registro de subclasses, um padrão em que as subclasses se registram com sua classe pai, eliminando a necessidade de importações diretas de subclasses.

Você também usou a função __import__() para importações dinâmicas, carregando módulos em tempo de execução somente quando necessário. Isso torna o código mais flexível e ajuda a evitar dependências circulares. Essas técnicas são essenciais para criar pacotes Python manteníveis com relacionamentos complexos de módulos e são comumente usadas em frameworks e bibliotecas. Aplicar esses padrões aos seus projetos pode ajudá-lo a construir estruturas de código mais modulares, extensíveis e manteníveis.