¿Importación circular de Python?

98

Entonces recibo este error

Traceback (most recent call last):
  File "/Users/alex/dev/runswift/utils/sim2014/simulator.py", line 3, in <module>
    from world import World
  File "/Users/alex/dev/runswift/utils/sim2014/world.py", line 2, in <module>
    from entities.field import Field
  File "/Users/alex/dev/runswift/utils/sim2014/entities/field.py", line 2, in <module>
    from entities.goal import Goal
  File "/Users/alex/dev/runswift/utils/sim2014/entities/goal.py", line 2, in <module>
    from entities.post import Post
  File "/Users/alex/dev/runswift/utils/sim2014/entities/post.py", line 4, in <module>
    from physics import PostBody
  File "/Users/alex/dev/runswift/utils/sim2014/physics.py", line 21, in <module>
    from entities.post import Post
ImportError: cannot import name Post

y puede ver que utilizo la misma declaración de importación más arriba y funciona? ¿Existe alguna regla no escrita sobre la importación circular? ¿Cómo uso la misma clase más abajo en la pila de llamadas?

CpILL
fuente

Respuestas:

161

Creo que la respuesta de jpmc26, aunque de ninguna manera es incorrecta , se reduce demasiado a las importaciones circulares. Pueden funcionar bien, si los configura correctamente.

La forma más sencilla de hacerlo es utilizar import my_modulesintaxis, en lugar de from my_module import some_object. El primero casi siempre funcionará, incluso si my_moduleincluido nos importa de nuevo. Este último solo funciona si my_objectya está definido en my_module, lo que en una importación circular puede no ser el caso.

Para ser específico para su caso: intente cambiar entities/post.pyde hacer import physicsy luego consulte en physics.PostBodylugar de hacerlo PostBodydirectamente. De manera similar, cambie physics.pypara hacer import entities.posty luego use en entities.post.Postlugar de solo Post.

Blckknght
fuente
5
¿Es esta respuesta compatible con las importaciones relativas?
Joe
17
¿Por qué pasó esto?
Juan Pablo Santos
4
Es incorrecto decir que la no fromsintaxis siempre funcionará. Si tengo class A(object): pass; class C(b.B): passen el módulo A y class B(a.A): passen el módulo B, entonces la importación circular sigue siendo un problema y esto no funcionará.
CrazyCasta
1
Tiene razón, cualquier dependencia circular en el código de nivel superior de los módulos (como las clases base de declaraciones de clase en su ejemplo) será un problema. Ese es el tipo de situación en la que la respuesta de jpmc de que debería refactorizar la organización del módulo es probablemente 100% correcta. Mueva la clase Bal módulo ao mueva la clase Cal módulo bpara que pueda romper el ciclo. También vale la pena señalar que incluso si solo una dirección del círculo tiene código de nivel superior involucrado (por ejemplo, si la clase Cno existe), es posible que obtenga un error, dependiendo de qué módulo fue importado primero por otro código.
Blckknght
2
@TylerCrompton: No estoy seguro de lo que quiere decir con "la importación de módulos debe ser absoluta". Las importaciones relativas circulares pueden funcionar, siempre que esté importando módulos, no su contenido (por ejemplo from . import sibling_module, no from .sibling_module import SomeClass). Hay algo más de sutileza cuando el __init__.pyarchivo de un paquete está involucrado en la importación circular, pero el problema es poco común y probablemente un error en la importimplementación. Vea el error 23447 de Python , para el que envié un parche (que lamentablemente ha estado languideciendo).
Blckknght
51

Cuando importa un módulo (o un miembro de él) por primera vez, el código dentro del módulo se ejecuta secuencialmente como cualquier otro código; por ejemplo, no se trata de forma diferente al cuerpo de una función. Una importes simplemente un comando como cualquier otra (asignación, una llamada de función, def, class). Suponiendo que sus importaciones se produzcan en la parte superior del script, esto es lo que está sucediendo:

  • Cuando intenta importar Worlddesde world, worldse ejecuta el script.
  • El worldscript se importa Field, lo que hace entities.fieldque se ejecute.
  • Este proceso continúa hasta que llega al entities.postscript porque intentó importarPost
  • El entities.postscript hace physicsque se ejecute el módulo porque intenta importarPostBody
  • Finalmente, physicsintenta importar Postdesdeentities.post
  • No estoy seguro de si el entities.postmódulo existe todavía en la memoria, pero realmente no importa. O el módulo no está en la memoria o el módulo aún no tiene un Postmiembro porque no ha terminado de ejecutarse para definirPost
  • De cualquier manera, se produce un error porque Postno está allí para ser importado.

Entonces no, no está "trabajando más arriba en la pila de llamadas". Este es un seguimiento de la pila de dónde ocurrió el error, lo que significa que se produjo un error al intentar importar Posten esa clase. No debería utilizar importaciones circulares. En el mejor de los casos, tiene un beneficio insignificante (normalmente, ningún beneficio) y causa problemas como este. Carga a cualquier revelador manteniéndola, obligándolos a caminar sobre cáscaras de huevo para no romperla. Refactorice la organización de su módulo.

jpmc26
fuente
1
Debería ser isinstance(userData, Post). Independientemente, no tienes otra opción. La importación circular no funcionará. El hecho de que tenga importaciones circulares es un olor a código para mí. Sugiere que tiene alguna funcionalidad que debería trasladarse a un tercer módulo. No podría decir qué sin mirar ambas clases enteras.
jpmc26
3
@CpILL Después de un tiempo, se me ocurrió una opción muy peligrosa. Si no puede hacer esto por ahora (debido a limitaciones de tiempo o lo que sea), entonces puede hacer su importación localmente dentro del método donde lo está usando. El cuerpo de una función en el interior defno se ejecuta hasta que se llama a la función, por lo que la importación no se producirá hasta que usted realmente llame a la función. Para entonces, el imports debería funcionar ya que uno de los módulos se habría importado completamente antes de la llamada. Ese es un truco absolutamente repugnante, y no debería permanecer en su base de código durante un período de tiempo significativo.
jpmc26
15
Creo que su respuesta es demasiado dura sobre las importaciones circulares. Las importaciones circulares suelen funcionar si lo hace simplemente en import foolugar de hacerlo from foo import Bar. Eso es porque la mayoría de los módulos simplemente definen cosas (como funciones y clases) que se ejecutan más tarde. Los módulos que hacen cosas importantes cuando los importa (como un script no protegido por if __name__ == "__main__") pueden seguir siendo un problema, pero eso no es demasiado común.
Blckknght
6
@Blckknght Creo que se está preparando para dedicar tiempo a problemas extraños que otras personas tendrán que investigar y que los confundirán si usa importaciones circulares. Te obligan a dedicar tiempo a tener cuidado de no tropezar con ellos, y encima hay un olor a código que tu diseño necesita refactorizar. Podría haberme equivocado acerca de si son técnicamente factibles, pero son una elección de diseño terrible destinada a causar problemas tarde o temprano. La claridad y la simplicidad son santos griales en la programación, y las importaciones circulares violan ambas en mi libro.
jpmc26
6
Alternativamente; ha dividido demasiado su funcionalidad y esa es la causa de las importaciones circulares. Si tiene dos cosas que dependen una de la otra todo el tiempo ; puede ser mejor ponerlos en un solo archivo. Python no es Java; no hay razón para no agrupar funciones / clases en un solo archivo para evitar una lógica de importación extraña. :-)
Mark Ribau
40

Para comprender las dependencias circulares, debe recordar que Python es esencialmente un lenguaje de secuencias de comandos. La ejecución de declaraciones fuera de los métodos se produce en tiempo de compilación. Las instrucciones de importación se ejecutan como llamadas a métodos, y para comprenderlas, debe pensar en ellas como llamadas a métodos.

Cuando realiza una importación, lo que sucede depende de si el archivo que está importando ya existe en la tabla del módulo. Si es así, Python usa lo que esté actualmente en la tabla de símbolos. Si no es así, Python comienza a leer el archivo del módulo, compilando / ejecutando / importando lo que encuentre allí. Los símbolos a los que se hace referencia en el momento de la compilación se encuentran o no, dependiendo de si se han visto o el compilador aún no los ha visto.

Imagina que tienes dos archivos fuente:

Archivo X.py

def X1:
    return "x1"

from Y import Y2

def X2:
    return "x2"

Archivo Y.py

def Y1:
    return "y1"

from X import X1

def Y2:
    return "y2"

Ahora suponga que compila el archivo X.py. El compilador comienza definiendo el método X1 y luego ingresa a la declaración de importación en X.py. Esto hace que el compilador pause la compilación de X.py y comience a compilar Y.py. Poco después, el compilador accede a la declaración de importación en Y.py. Dado que X.py ya está en la tabla de módulos, Python usa la tabla de símbolos X.py incompleta existente para satisfacer las referencias solicitadas. Todos los símbolos que aparecen antes de la declaración de importación en X.py están ahora en la tabla de símbolos, pero los símbolos posteriores no. Dado que X1 ahora aparece antes de la declaración de importación, se importa correctamente. Python luego reanuda la compilación de Y.py. Al hacerlo, define Y2 y termina de compilar Y.py. Luego reanuda la compilación de X.py y encuentra Y2 en la tabla de símbolos Y.py. La compilación finalmente se completa sin errores.

Algo muy diferente sucede si intenta compilar Y.py desde la línea de comandos. Mientras compila Y.py, el compilador llega a la declaración de importación antes de definir Y2. Luego comienza a compilar X.py. Pronto llega a la declaración de importación en X.py que requiere Y2. Pero Y2 no está definido, por lo que la compilación falla.

Tenga en cuenta que si modifica X.py para importar Y1, la compilación siempre se realizará correctamente, independientemente del archivo que compile. Sin embargo, si modifica el archivo Y.py para importar el símbolo X2, ninguno de los archivos se compilará.

En cualquier momento en que el módulo X, o cualquier módulo importado por X pueda importar el módulo actual, NO utilice:

from X import Y

Siempre que crea que puede haber una importación circular, también debe evitar compilar referencias de tiempo a variables en otros módulos. Considere el código de apariencia inocente:

import X
z = X.Y

Suponga que el módulo X importa este módulo antes de que este módulo importe X. Además, suponga que Y se define en X después de la declaración de importación. Entonces, Y no se definirá cuando se importe este módulo y obtendrá un error de compilación. Si este módulo importa Y primero, puede salirse con la suya. Pero cuando uno de sus compañeros de trabajo cambia inocentemente el orden de las definiciones en un tercer módulo, el código se romperá.

En algunos casos, puede resolver dependencias circulares moviendo una declaración de importación hacia abajo debajo de las definiciones de símbolos que necesitan otros módulos. En los ejemplos anteriores, las definiciones antes de la declaración de importación nunca fallan. Las definiciones posteriores a la declaración de importación a veces fallan, según el orden de compilación. Incluso puede colocar declaraciones de importación al final de un archivo, siempre que no se necesite ninguno de los símbolos importados en el momento de la compilación.

Tenga en cuenta que mover las declaraciones de importación hacia abajo en un módulo oscurece lo que está haciendo. Compensa esto con un comentario en la parte superior de tu módulo, algo como lo siguiente:

#import X   (actual import moved down to avoid circular dependency)

En general, esta es una mala práctica, pero a veces es difícil de evitar.

Gene Olson
fuente
2
No creo que haya tiempo de compilación o compilación en Python en absoluto
pkqxdd
6
Python hace tener un compilador, y está compilado @pkqxdd, compilación se acaba generalmente oculto lejos del usuario. Esto puede resultar un poco confuso, pero sería difícil para el autor dar esta descripción admirablemente clara de lo que está sucediendo sin alguna referencia al "tiempo de compilación" de Python, algo oscurecido.
Hank
Seguí adelante para probar esto en mi máquina y obtuve un resultado diferente. Ejecutó X.py pero obtuvo el error "no se puede importar el nombre 'Y2' de 'Y'". Sin embargo, corrió Y.py sin problemas. Estoy en Python 3.7.5. ¿Podrían ayudarme a explicar cuál es el problema aquí?
xuefeng huang
18

Para aquellos de ustedes que, como yo, ven este problema desde Django, deben saber que los documentos brindan una solución: https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

"... Para hacer referencia a los modelos definidos en otra aplicación, puede especificar explícitamente un modelo con la etiqueta de la aplicación completa. Por ejemplo, si el modelo de fabricante anterior se define en otra aplicación llamada producción, deberá utilizar:

class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',
        on_delete=models.CASCADE,
)

Este tipo de referencia puede resultar útil para resolver dependencias de importación circulares entre dos aplicaciones. ... "

Malik A. Rumi
fuente
6
Sé que se supone que no debo usar un comentario para decir "gracias", pero esto me ha estado atormentando durante algunas horas. ¡¡¡Gracias, gracias, gracias!!!
MikeyE
Estoy de acuerdo con @MikeyE. He leído varios blogs y Stackoverflows tratando de remediar esto con PonyORM. Donde otros dicen que es una mala práctica, o por qué codificarías tus clases para que sean circulares, bueno, los ORM son exactamente donde sucede esto. Debido a que muchos ejemplos colocan todos los modelos en el mismo archivo, y seguimos esos ejemplos, excepto que usamos un modelo por archivo, el problema no está claro cuando Python no se compila. Sin embargo, la respuesta es muy sencilla. Como señaló Mike, muchas gracias.
trash80
3

Pude importar el módulo dentro de la función (solo) que requeriría los objetos de este módulo:

def my_func():
    import Foo
    foo_instance = Foo()
Alexander Shubert
fuente
qué elegante de pitón
Yaro
2

Si se encuentra con este problema en una aplicación bastante compleja, puede resultar engorroso refactorizar todas sus importaciones. PyCharm ofrece una solución rápida para esto que también cambiará automáticamente todo el uso de los símbolos importados.

ingrese la descripción de la imagen aquí

Andreas Bergström
fuente
0

Estaba usando lo siguiente:

from module import Foo

foo_instance = Foo()

pero para deshacerme de circular referencehice lo siguiente y funcionó:

import module.foo

foo_instance = foo.Foo()
MKJ
fuente