Щоб зрозуміти кругові залежності, потрібно пам’ятати, що 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)
Як правило, це погана практика, але іноді її складно уникнути.