Importaciones circulares de Python

Una dependencia circular ocurre cuando dos o más módulos dependen uno del otro. Esto se debe a que cada módulo se define en términos del otro (Ver Figu...

¿Qué es una dependencia circular?

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

Por ejemplo:

1
2
functionA():
    functionB()

Y

1
2
functionB():
    functionA()

El código anterior representa una dependencia circular bastante obvia. funciónA() llama a funciónB(), por lo tanto dependiendo de ella, y funciónB() llama a funciónA(). Este tipo de dependencia circular tiene algunos problemas obvios, que describiremos un poco más en la siguiente sección.

{.img-responsive}

Figura 1

Problemas con 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 capacidad de reutilización reducida del código. 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 fallas potenciales, como recurrencias infinitas, fugas 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:

1
2
3
4
5
6
7
8
# module1
import module2

def function1():
    module2.function2()

def function3():
    print('Goodbye, World!')
1
2
3
4
5
6
# module2
import module1

def function2():
    print('Hello, World!')
    module1.function3()
1
2
3
4
5
# __init__.py

import module1

module1.function1()

Cuando Python importa un módulo, verifica 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 del caché. El registro de módulos 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 estaba registrado, Python encuentra 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 llega a importar módulo2, lo carga y lo ejecuta. Sin embargo, module2 también llama a module1, que a su vez define function1().

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ 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 solucionar 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 en realidad no es 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 se pueden fusionar en un solo módulo más grande. El código resultante de nuestro ejemplo anterior se vería así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# module 1 & 2

def function1():
    function2()

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

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

function1()

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

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

1
2
3
4
5
6
7
8
# module 1

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

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

En este caso, Python podrá cargar todas las funciones en el módulo 1 y luego cargar el módulo 2 solo cuando sea necesario.

Este enfoque no contradice la sintaxis de Python, como dice La documentación de Python dice: "Es habitual, pero no obligatorio, colocar todos import declaraciones al comienzo de un módulo (o script, para el caso)".

La documentación de Python también dice que es recomendable 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, dependiendo de su proyecto) .

Terminando

Las importaciones circulares son un caso específico de las referencias circulares. En general, 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 topado con importaciones circulares en su propio código? Si es así, ¿cómo lo arreglaste? ¡Cuéntanos en los comentarios!*
Licensed under CC BY-NC-SA 4.0