Importaciones circulares de Python

    驴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!

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *