У меня проблемы с пониманием того, как python управляет import
s.
Допустим, у меня есть следующая структура приложения:
application/
- application.py
- model/
-- __init__.py
-- user.py
Допустим, файл application.py
импортирует модуль модели после создания базы данных следующим образом:
db = SQLAlchemy(application)
import model
Допустим также, что модуль model
импортирует файл user.py
следующим образом:
import user
Наконец, предположим, что файл user.py
импортирует экземпляр db
из файла application.py
следующим образом:
from application import db
Мне это кажется круговой зависимостью, поскольку файлу application.py
косвенно требуется файл user.py
, но файлу user.py
требуется экземпляр db
из файла application.py
.
Я знаю, что этот код действительно работает, как я его тестировал, но может ли кто-нибудь объяснить, как именно Python обрабатывает это и когда он завершает циклический цикл.
Подводя итог проблеме, когда user.py
файл импортирует db
из файла application.py
, мне кажется, что он также вызывает модуль импорта model
, который создает бесконечный цикл.
Циркулярный импорт сам по себе не обязательно является проблемой. Только циклические зависимости. Вы можете увидеть, как решается круговой импорт, поэкспериментировав:
В mymod1.py
import sys
print("1. from mymod1:", [x for x in sys.modules if x.startswith("mymod")])
import mymod2
print("2. from mymod1:", [x for x in sys.modules if x.startswith("mymod")])
В mymod2.py
import sys
print("1. from mymod2:", [x for x in sys.modules if x.startswith("mymod")])
import mymod3
print("2. from mymod2:", [x for x in sys.modules if x.startswith("mymod")])
В mymod3.py
import sys
print("1. from mymod3:", [x for x in sys.modules if x.startswith("mymod")])
import mymod1
print("2. from mymod3:", [x for x in sys.modules if x.startswith("mymod")])
Теперь у нас есть цикл mymod1 -> mymod2 -> mymod3 -> mymod1
. Войдите в REPL и посмотрите, что произойдет:
>>> import mymod1
1. from mymod1: ['mymod1'] # before mymod1 imported mymod2. note mymod1 is already there!
1. from mymod2: ['mymod1', 'mymod2'] # in mymod2 now, before importing mymod3
1. from mymod3: ['mymod1', 'mymod2', 'mymod3'] # before mymod3 imports mymod1
2. from mymod3: ['mymod1', 'mymod2', 'mymod3'] # mymod3 exit
2. from mymod2: ['mymod1', 'mymod2', 'mymod3'] # mymod2 exit
2. from mymod1: ['mymod1', 'mymod3', 'mymod2'] # mymod1 exit
Ключевым моментом здесь является то, что сам экземпляр модуля уже присутствует в sys.modules
до того, как он завершит выполнение. Это означает, что его можно импортировать снова, и он вернет существующий объект без необходимости повторного выполнения всего кода на уровне модуля.
Для компонентов внутри подмодулей пакета естественно иметь взаимозависимости. Проблемы в основном возникают, когда код с областью действия модуля начинает фактически выполнять такие действия, как попытка подключения к базе данных, поэтому старайтесь избегать написания каких-либо сценариев непосредственно на уровне модуля.
Причиной циклических ошибок импорта является то, что что-то в пространстве имен модуля необходимо до того, как модуль завершит инициализацию. В этом случае сам модуль существует, но имя, к которому вы пытаетесь получить доступ, может еще не существовать.
# mymod.py
import sys
print("var" in vars(sys.modules["mymod"]))
var = "I'm a name in mymod namespace"
print("var" in vars(sys.modules["mymod"]))
Импорт mymod
напечатает False
, а затем True
, само пространство имен модуля все еще изменяется, поскольку импорт находится в процессе выполнения.
Внимательный читатель мог заметить, что mymod2
и mymod3
поменялись местами в выводе:
2. from mymod2: ['mymod1', 'mymod2', 'mymod3']
2. from mymod1: ['mymod1', 'mymod3', 'mymod2']
|_____ wtf? _____|
На самом деле это не случайно! Механизм импорта, как последний шаг в загрузке модуля, извлекает фактический модуль из sys.modules
. Если вы снова проверите в REPL, mymod1
теперь будет последним.
>>> import mymod1
...
>>> [x for x in sys.modules if x.startswith("mymod")]
['mymod3', 'mymod2', 'mymod1']
Я не буду описывать, почему система импорта так делает, потому что это не совсем относится к вопросу, но пользователи, которым интересно узнать причины, должны увидеть этот пост в списке рассылки от Гвидо.