Чтобы понять циклические зависимости, вам нужно помнить, что Python - это, по сути, язык сценариев. Выполнение операторов вне методов происходит во время компиляции. Операторы импорта выполняются так же, как вызовы методов, и чтобы понять их, вы должны думать о них как о вызовах методов.
Что происходит при импорте, зависит от того, существует ли уже импортируемый файл в таблице модуля. Если это так, Python использует все, что в данный момент находится в таблице символов. Если нет, Python начинает читать файл модуля, компилируя / выполняя / импортируя все, что он там находит. Символы, на которые ссылаются во время компиляции, обнаруживаются или нет, в зависимости от того, были ли они видны компилятору или еще не увидены компилятором.
Представьте, что у вас есть два исходных файла:
Файл X.py
def X1:
return "x1"
from Y import Y2
def X2:
return "x2"
Файл Y.py
def Y1:
return "y1"
from X import X1
def Y2:
return "y2"
Теперь предположим, что вы компилируете файл X.py. Компилятор начинает с определения метода X1, а затем выполняет оператор импорта в X.py. Это заставляет компилятор приостановить компиляцию X.py и начать компиляцию Y.py. Вскоре после этого компилятор выполняет оператор импорта в Y.py. Поскольку X.py уже находится в таблице модулей, Python использует существующую неполную таблицу символов X.py для удовлетворения любых запрошенных ссылок. Любые символы, появляющиеся перед оператором импорта в X.py, теперь находятся в таблице символов, а символы после них - нет. Поскольку X1 теперь появляется перед оператором импорта, он успешно импортирован. Затем Python возобновляет компиляцию Y.py. При этом он определяет Y2 и завершает компиляцию Y.py. Затем он возобновляет компиляцию X.py и находит Y2 в таблице символов Y.py. В конечном итоге компиляция завершается без ошибки.
Что-то совсем другое произойдет, если вы попытаетесь скомпилировать Y.py из командной строки. При компиляции Y.py компилятор выполняет оператор импорта до того, как он определит Y2. Затем начинается компиляция X.py. Вскоре он попадает в оператор импорта в X.py, который требует Y2. Но Y2 не определен, поэтому компиляция не выполняется.
Обратите внимание, что если вы измените X.py для импорта Y1, компиляция всегда будет успешной, независимо от того, какой файл вы компилируете. Однако если вы измените файл Y.py для импорта символа X2, ни один файл не будет компилироваться.
Каждый раз, когда модуль X или любой модуль, импортированный X, может импортировать текущий модуль, НЕ используйте:
from X import Y
Каждый раз, когда вы думаете, что может иметь место циклический импорт, вам также следует избегать ссылок на переменные во время компиляции в других модулях. Рассмотрим невинно выглядящий код:
import X
z = X.Y
Предположим, что модуль X импортирует этот модуль до того, как этот модуль импортирует X. Далее предположим, что Y определен в X после оператора импорта. Тогда Y не будет определяться при импорте этого модуля, и вы получите ошибку компиляции. Если этот модуль сначала импортирует Y, вы можете обойтись без него. Но когда один из ваших коллег невинно меняет порядок определений в третьем модуле, код ломается.
В некоторых случаях вы можете разрешить циклические зависимости, переместив оператор импорта вниз под определениями символов, необходимых для других модулей. В приведенных выше примерах определения перед оператором импорта никогда не ошибаются. Определения после оператора импорта иногда терпят неудачу, в зависимости от порядка компиляции. Вы даже можете поместить операторы импорта в конец файла, если ни один из импортированных символов не нужен во время компиляции.
Обратите внимание, что перемещение операторов импорта вниз в модуле скрывает то, что вы делаете. Компенсируйте это с помощью комментария в верхней части модуля примерно следующего содержания:
#import X (actual import moved down to avoid circular dependency)
В целом это плохая практика, но иногда ее трудно избежать.