¿Por qué las importaciones circulares aparentemente funcionan más arriba en la pila de llamadas pero luego generan un ImportError más abajo?
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 puedes ver que uso 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?
Consulte también ¿Qué sucede cuando se utilizan importaciones mutuas o circulares (cíclicas) en Python? para obtener una descripción general de lo que está permitido y lo que causa un problema en las importaciones circulares de WRT. Consulte ¿Qué puedo hacer con "ImportError: no se puede importar el nombre X" o "AttributeError: ... (probablemente debido a una importación circular)"? para técnicas para resolver y evitar dependencias circulares.
Creo que la respuesta de jpmc26 , aunque de ninguna manera es incorrecta , se refiere demasiado a las importaciones circulares. Pueden funcionar bien si los configuras correctamente.
La forma más sencilla de hacerlo es utilizar import my_module
la sintaxis, en lugar de from my_module import some_object
. El primero casi siempre funcionará, incluso si my_module
nos importa incluido. Este último solo funciona si my_object
ya está definido en my_module
, lo que en una importación circular puede no ser el caso.
Para ser específico en su caso: intente cambiar entities/post.py
a hacer import physics
y luego consulte physics.PostBody
en lugar de hacerlo PostBody
directamente. De manera similar, cambie physics.py
a do import entities.post
y luego use entities.post.Post
en lugar de simplemente Post
.
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 manera diferente que el cuerpo de una función. An import
es solo un comando como cualquier otro (asignación, llamada a función, def
, class
). Suponiendo que sus importaciones ocurren en la parte superior del script, esto es lo que sucede:
- Cuando intenta importar
World
desdeworld
, elworld
script se ejecuta. - El
world
script importaField
, lo que hace que elentities.field
script se ejecute. - Este proceso continúa hasta llegar al
entities.post
script porque intentaste importarPost
- El
entities.post
script hace quephysics
el módulo se ejecute porque intenta importarPostBody
- Finalmente,
physics
intenta importarPost
desdeentities.post
- No estoy seguro de si el
entities.post
mó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 unPost
miembro porque no ha terminado de ejecutarse para definirPost
- De cualquier manera, se produce un error porque
Post
no 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 Post
en esa clase. No deberías utilizar importaciones circulares. En el mejor de los casos, tiene un beneficio insignificante (normalmente, ningún beneficio) y causa problemas como este. Es una carga para cualquier desarrollador que lo mantenga, obligándolo a caminar sobre cáscaras de huevo para evitar romperlo. Refactorice la organización de su módulo.
Para comprender las dependencias circulares, debe recordar que Python es esencialmente un lenguaje de programación. La ejecución de declaraciones fuera de los métodos ocurre en tiempo de compilación. Las declaraciones de importación se ejecutan como llamadas a métodos y, para comprenderlas, debes 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. De lo contrario, 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 el compilador los ha visto o 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 supongamos que compila el archivo X.py. El compilador comienza definiendo el método X1 y luego accede a la declaración de importación en X.py. Esto hace que el compilador detenga la compilación de X.py y comience a compilar Y.py. Poco después, el compilador llega a la declaración de importación en Y.py. Dado que X.py ya está en la tabla de módulos, Python utiliza la tabla de símbolos X.py incompleta existente para satisfacer cualquier referencia solicitada. Todos los símbolos que aparecen antes de la declaración de importación en X.py ahora están 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 importó correctamente. Luego, Python continúa compilando 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 de Y.py. La compilación finalmente se completa sin errores.
Algo muy diferente sucede si intentas compilar Y.py desde la línea de comando. Mientras compila Y.py, el compilador accede 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, sin importar qué archivo 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
Cada vez que crea que puede haber una importación circular, también debe evitar las referencias en tiempo de compilación a variables en otros módulos. Considere el código de aspecto inocente:
import X
z = X.Y
Supongamos que el módulo X importa este módulo antes de que este módulo importe X. Además, supongamos que Y está definido 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, puedes salirte con la tuya. 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 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 después de 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 ninguno de los símbolos importados sea necesario 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. Compense esto con un comentario en la parte superior de su módulo, similar al 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 evitarla.
Para aquellos de ustedes que, como yo, llegan a 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 modelos definidos en otra aplicación, puede especificar explícitamente un modelo con la etiqueta de aplicación completa. Por ejemplo, si el modelo del Fabricante anterior está definido en otra aplicación llamada producción, deberá usar:
class Car(models.Model):
manufacturer = models.ForeignKey(
'production.Manufacturer',
on_delete=models.CASCADE,
)
Este tipo de referencia puede resultar útil al resolver dependencias de importación circular entre dos aplicaciones. ..."