Importaciones circulares de Python

I

¿Qué es una dependencia circular?

Una dependencia circular ocurre cuando dos o más módulos dependen entre sí. Esto se debe al hecho de que cada módulo se define en términos del otro (Ver Figura 1).

Por ejemplo:

functionA():
    functionB()

Y

functionB():
    functionA()

El código de arriba muestra una dependencia circular bastante obvia. functionA() llamadas functionB(), por tanto dependiendo de ello, y functionB() llamadas functionA(). Este tipo de dependencia circular tiene algunos problemas obvios, que describiremos un poco más en la siguiente sección.

Figura 1

Problemas con las dependencias circulares

Las dependencias circulares pueden causar bastantes problemas en su código. Por ejemplo, puede generar un acoplamiento estrecho entre módulos y, como consecuencia, una reutilización de código reducida. Este hecho también hace que el código sea más difícil de mantener a largo plazo.

Además, las dependencias circulares pueden ser la fuente de posibles fallas, como recurrencias infinitas, pérdidas de memoria y efectos en cascada. Si no tiene cuidado y tiene una dependencia circular en su código, puede ser muy difícil depurar los muchos problemas potenciales que causa.

¿Qué es una importación circular?

La importación circular es una forma de dependencia circular que se crea con la declaración de importación en Python.

Por ejemplo, analicemos el siguiente código:

# module1
import module2

def function1():
    module2.function2()

def function3():
    print('Goodbye, World!')
# module2
import module1

def function2():
    print('Hello, World!')
    module1.function3()
# __init__.py

import module1

module1.function1()

Cuando Python importa un módulo, comprueba el registro del módulo para ver si el módulo ya se importó. Si el módulo ya estaba registrado, Python usa ese objeto existente de la caché. El registro del módulo es una tabla de módulos que se han inicializado e indexado por nombre de módulo. Se puede acceder a esta tabla a través de sys.modules.

Si no se registró, Python busca el módulo, lo inicializa si es necesario y lo ejecuta en el espacio de nombres del nuevo módulo.

En nuestro ejemplo, cuando Python alcanza import module2, lo carga y lo ejecuta. Sin embargo, module2 también requiere module1, que a su vez define function1().

El problema ocurre cuando function2() intenta llamar al módulo1 function3(). Dado que el módulo1 se cargó primero y, a su vez, se cargó el módulo2 antes de que pudiera alcanzar function3(), esa función aún no está definida y arroja un error cuando se llama:

$ python __init__.py
Hello, World!
Traceback (most recent call last):
  File "__init__.py", line 3, in <module>
    module1.function1()
  File "/Users/scott/projects/sandbox/python/circular-dep-test/module1/__init__.py", line 5, in function1
    module2.function2()
  File "/Users/scott/projects/sandbox/python/circular-dep-test/module2/__init__.py", line 6, in function2
    module1.function3()
AttributeError: 'module' object has no attribute 'function3'

Cómo arreglar las dependencias circulares

En general, las importaciones circulares son el resultado de malos diseños. Un análisis más profundo del programa podría haber concluido que la dependencia no es realmente necesaria, o que la funcionalidad dependiente se puede mover a diferentes módulos que no contendrían la referencia circular.

Una solución simple es que, a veces, ambos módulos pueden fusionarse en un solo módulo más grande. El código resultante de nuestro ejemplo anterior se vería así:

# module 1 & 2

def function1():
    function2()

def function2():
    print('Hello, World!')
    function3()

def function3():
    print('Goodbye, World!')

function1()

Sin embargo, el módulo combinado puede tener algunas funciones no relacionadas (acoplamiento estrecho) y podría volverse muy grande si los dos módulos ya tienen mucho código en ellos.

Entonces, si eso no funciona, otra solución podría haber sido diferir la importación de module2 para importarlo solo cuando sea necesario. Esto se puede hacer colocando la importación de module2 dentro de la definición de function1():

# module 1

def function1():
    import module2
    module2.function2()

def function3():
    print('Goodbye, World!')

En este caso, Python podrá cargar todas las funciones en module1 y luego cargar module2 solo cuando sea necesario.

Este enfoque no contradice la sintaxis de Python, ya que el La documentación de Python dice: “Es habitual, pero no obligatorio, colocar todas las declaraciones de importación al principio de un módulo (o script, para el caso)”.

La documentación de Python también dice que es aconsejable utilizar import X, en lugar de otras declaraciones, como from module import *o from module import a,b,c.

También puede ver muchas bases de código que utilizan la importación diferida incluso si no hay una dependencia circular, lo que acelera el tiempo de inicio, por lo que esto no se considera una mala práctica en absoluto (aunque puede ser un mal diseño, según su proyecto) .

Terminando

Las importaciones circulares son un caso específico de referencias circulares. Generalmente, se pueden resolver con un mejor diseño de código. Sin embargo, a veces, el diseño resultante puede contener una gran cantidad de código o mezclar funcionalidades no relacionadas (acoplamiento estrecho).

¿Se ha encontrado con importaciones circulares en su propio código? Si es así, ¿cómo lo arreglaste? ¡Háznoslo saber en los comentarios!

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad