Importaciones circulares y dinámicas de módulos

PythonPythonBeginner
Practicar Ahora

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

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este laboratorio, aprenderás sobre dos conceptos cruciales relacionados con las importaciones en Python. Las importaciones de módulos en Python a veces pueden resultar en dependencias complejas, lo que puede llevar a errores o estructuras de código ineficientes. Las importaciones circulares, en las que dos o más módulos se importan entre sí, crean un bucle de dependencia que puede causar problemas si no se gestionan adecuadamente.

También explorarás las importaciones dinámicas, que permiten cargar módulos en tiempo de ejecución en lugar de al inicio del programa. Esto proporciona flexibilidad y ayuda a evitar problemas relacionados con las importaciones. Los objetivos de este laboratorio son comprender los problemas de importación circular, implementar soluciones para evitarlos y aprender a utilizar eficazmente las importaciones dinámicas de módulos.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") subgraph Lab Skills python/importing_modules -.-> lab-132531{{"Importaciones circulares y dinámicas de módulos"}} python/classes_objects -.-> lab-132531{{"Importaciones circulares y dinámicas de módulos"}} python/inheritance -.-> lab-132531{{"Importaciones circulares y dinámicas de módulos"}} end

Comprendiendo el problema de importación

Comencemos por entender qué son las importaciones de módulos. En Python, cuando quieres utilizar funciones, clases o variables de otro archivo (módulo), se utiliza la declaración import. Sin embargo, la forma en que estructuras tus importaciones puede generar diversos problemas.

Ahora, vamos a examinar un ejemplo de una estructura de módulo problemática. El código en tableformat/formatter.py tiene importaciones dispersas a lo largo del archivo. Esto puede no parecer un gran problema al principio, pero crea problemas de mantenimiento y dependencias.

Primero, abre el explorador de archivos del WebIDE y navega hasta el directorio structly. Ejecutaremos un par de comandos para entender la estructura actual del proyecto. El comando cd se utiliza para cambiar el directorio de trabajo actual, y el comando ls -la lista todos los archivos y directorios en el directorio actual, incluyendo los ocultos.

cd ~/project/structly
ls -la

Esto mostrará los archivos en el directorio del proyecto. Ahora, vamos a ver uno de los archivos problemáticos utilizando el comando cat, que muestra el contenido de un archivo.

cat tableformat/formatter.py

Deberías ver un código similar al siguiente:

## 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()

Observa la ubicación de las declaraciones de importación en medio del archivo. Esto es problemático por varios motivos:

  1. Hace que el código sea más difícil de leer y mantener. Cuando se mira un archivo, se espera ver todas las importaciones al principio para poder entender rápidamente de qué módulos externos depende el archivo.
  2. Puede generar problemas de importación circular. Las importaciones circulares ocurren cuando dos o más módulos dependen entre sí, lo que puede causar errores y hacer que tu código se comporte de manera inesperada.
  3. Rompe la convención de Python de colocar todas las importaciones en la parte superior de un archivo. Seguir las convenciones hace que tu código sea más legible y más fácil de entender para otros desarrolladores.

En los siguientes pasos, exploraremos estos problemas en más detalle y aprenderemos cómo resolverlos.

Explorando las importaciones circulares

Una importación circular es una situación en la que dos o más módulos dependen entre sí. Específicamente, cuando el módulo A importa el módulo B, y el módulo B también importa el módulo A, ya sea directamente o indirectamente. Esto crea un bucle de dependencia que el sistema de importación de Python no puede resolver adecuadamente. En términos más simples, Python se queda atrapado en un bucle tratando de averiguar qué módulo importar primero, y esto puede causar errores en tu programa.

Vamos a experimentar con nuestro código para ver cómo las importaciones circulares pueden causar problemas.

Primero, ejecutaremos el programa de stocks (acciones) para comprobar si funciona con la estructura actual. Este paso nos ayuda a establecer una base y ver el programa funcionando como se espera antes de realizar cualquier cambio.

cd ~/project/structly
python3 stock.py

El programa debería ejecutarse correctamente y mostrar los datos de las acciones en una tabla formateada. Si lo hace, eso significa que la estructura actual del código funciona bien sin ningún problema de importación circular.

Ahora, vamos a modificar el archivo formatter.py. Por lo general, es una buena práctica mover las importaciones a la parte superior de un archivo. Esto hace que el código esté más organizado y sea más fácil de entender a simple vista.

cd ~/project/structly

Abre tableformat/formatter.py en el WebIDE. Vamos a mover las siguientes importaciones a la parte superior del archivo, justo después de las importaciones existentes. Estas importaciones son para diferentes formateadores de tablas, como texto, CSV y HTML.

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

Así que el comienzo del archivo ahora debería verse así:

## 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

Guarda el archivo y vuelve a intentar ejecutar el programa de stocks.

python3 stock.py

Deberías ver un mensaje de error sobre TableFormatter no definido. Esto es una señal clara de un problema de importación circular.

El problema se produce debido a la siguiente cadena de eventos:

  1. formatter.py intenta importar TextTableFormatter de formats/text.py.
  2. formats/text.py importa TableFormatter de formatter.py.
  3. Cuando Python intenta resolver estas importaciones, se queda atrapado en un bucle porque no puede decidir qué módulo importar por completo primero.

Vamos a revertir nuestros cambios para que el programa funcione de nuevo. Edita tableformat/formatter.py y mueve las importaciones de vuelta a donde estaban originalmente (después de la definición de la clase 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

Ejecuta el programa de nuevo para confirmar que está funcionando.

python3 stock.py

Esto demuestra que aunque tener importaciones en medio del archivo no es la mejor práctica en términos de organización del código, se hizo para evitar un problema de importación circular. En los siguientes pasos, exploraremos mejores soluciones.

Implementando el registro de subclases

En la programación, las importaciones circulares pueden ser un problema complicado. En lugar de importar directamente las clases de formateadores, podemos utilizar un patrón de registro. En este patrón, las subclases se registran a sí mismas con su clase padre. Esta es una forma común y efectiva de evitar las importaciones circulares.

Primero, entendamos cómo podemos averiguar el nombre del módulo de una clase. El nombre del módulo es importante porque lo usaremos en nuestro patrón de registro. Para hacer esto, ejecutaremos un comando de Python en la terminal.

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

Cuando ejecutes este comando, verás una salida como esta:

structly.tableformat.formats.text
text

Esta salida muestra que podemos extraer el nombre del módulo de la propia clase. Usaremos este nombre de módulo más adelante para registrar las subclases.

Ahora, modifiquemos la clase TableFormatter en el archivo tableformat/formatter.py para agregar un mecanismo de registro. Abre este archivo en el WebIDE. Agregaremos algún código a la clase TableFormatter. Este código nos ayudará a registrar las subclases automáticamente.

class TableFormatter(ABC):
    _formats = { }  ## Dictionary to store registered formatters

    @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

El método __init_subclass__ es un método especial en Python. Se llama siempre que se crea una subclase de TableFormatter. En este método, extraemos el nombre del módulo de la subclase y lo usamos como clave para registrar la subclase en el diccionario _formats.

A continuación, necesitamos modificar la función create_formatter para usar el diccionario de registro. Esta función es responsable de crear el formateador adecuado basado en el nombre dado.

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()

Después de hacer estos cambios, guarda el archivo. Luego, probemos si el programa sigue funcionando. Ejecutaremos el script stock.py.

python3 stock.py

Si el programa se ejecuta correctamente, significa que nuestros cambios no han roto nada. Ahora, echemos un vistazo al contenido del diccionario _formats para ver cómo funciona el registro.

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

Deberías ver una salida como esta:

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

Esta salida confirma que nuestras subclases se están registrando correctamente en el diccionario _formats. Sin embargo, todavía tenemos algunas importaciones en medio del archivo. En el siguiente paso, solucionaremos este problema utilizando importaciones dinámicas.

✨ Revisar Solución y Practicar

Usando importaciones dinámicas

En la programación, las importaciones se utilizan para traer código de otros módulos para que podamos utilizar su funcionalidad. Sin embargo, a veces tener importaciones en medio de un archivo puede hacer que el código sea un poco desordenado y difícil de entender. En esta parte, aprenderemos cómo usar importaciones dinámicas para resolver este problema. Las importaciones dinámicas son una característica poderosa que nos permite cargar módulos en tiempo de ejecución, lo que significa que solo cargamos un módulo cuando realmente lo necesitamos.

Primero, necesitamos eliminar las declaraciones de importación que actualmente se encuentran después de la clase TableFormatter. Estas importaciones son importaciones estáticas, que se cargan cuando el programa se inicia. Para hacer esto, abre el archivo tableformat/formatter.py en el WebIDE. Una vez que hayas abierto el archivo, encuentra y elimina las siguientes líneas:

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

Si intentas ejecutar el programa ahora ejecutando el siguiente comando en la terminal:

python3 stock.py

El programa fallará. La razón es que los formateadores no se registrarán en el diccionario _formats. Verás un mensaje de error sobre un formato desconocido. Esto se debe a que el programa no puede encontrar las clases de formateadores que necesita para funcionar correctamente.

Para solucionar este problema, modificaremos la función create_formatter. El objetivo es importar dinámicamente el módulo necesario cuando sea necesario. Actualiza la función como se muestra a continuación:

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()

La línea más importante en esta función es:

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

Esta línea importa dinámicamente el módulo basado en el nombre del formato. Cuando se importa el módulo, su subclase de TableFormatter se registra automáticamente. Esto se debe al método __init_subclass__ que agregamos anteriormente. Este método es un método especial de Python que se llama cuando se crea una subclase, y en nuestro caso, se utiliza para registrar la clase de formateador.

Después de hacer estos cambios, guarda el archivo. Luego, ejecuta el programa nuevamente utilizando el siguiente comando:

python3 stock.py

El programa ahora debería funcionar correctamente, incluso aunque hayamos eliminado las importaciones estáticas. Para verificar que la importación dinámica está funcionando como se espera, limpiaremos el diccionario _formats y luego llamaremos a la función create_formatter. Ejecuta el siguiente comando en la 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)"

Deberías ver una salida similar a esta:

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

Esta salida confirma que la importación dinámica está cargando el módulo y registrando la clase de formateador cuando es necesario.

Al usar importaciones dinámicas y registro de clases, hemos creado una estructura de código más limpia y fácil de mantener. Estos son los beneficios:

  1. Todas las importaciones ahora están en la parte superior del archivo, lo que sigue las convenciones de Python. Esto hace que el código sea más fácil de leer y entender.
  2. Hemos eliminado las importaciones circulares. Las importaciones circulares pueden causar problemas en un programa, como bucles infinitos o errores difíciles de depurar.
  3. El código es más flexible. Ahora, podemos agregar nuevos formateadores sin modificar la función create_formatter. Esto es muy útil en un escenario del mundo real donde se pueden agregar nuevas características con el tiempo.

Este patrón de uso de importaciones dinámicas y registro de clases se utiliza comúnmente en sistemas de complementos (plugins) y marcos de trabajo (frameworks). En estos sistemas, los componentes deben cargarse dinámicamente según las necesidades del usuario o los requisitos del programa.

✨ Revisar Solución y Practicar

Resumen

En este laboratorio (lab), has aprendido conceptos y técnicas cruciales sobre la importación de módulos en Python. Primero, has explorado las importaciones circulares, comprendiendo cómo las dependencias circulares entre módulos pueden causar problemas y por qué es necesario manejarlas con cuidado para evitarlas. Segundo, has implementado el registro de subclases, un patrón en el que las subclases se registran con su clase padre, eliminando la necesidad de importar directamente las subclases.

También has utilizado la función __import__() para importaciones dinámicas, cargando módulos en tiempo de ejecución solo cuando sea necesario. Esto hace que el código sea más flexible y ayuda a evitar dependencias circulares. Estas técnicas son esenciales para crear paquetes de Python mantenibles con relaciones complejas entre módulos y se utilizan comúnmente en marcos de trabajo (frameworks) y bibliotecas. Aplicar estos patrones a tus proyectos puede ayudarte a construir estructuras de código más modulares, extensibles y mantenibles.