Вы когда-нибудь сталкивались с круговым импортом в Python? Это очень распространенный запах кода, который указывает на то, что что-то не так с дизайном или структурой.
Пример кругового импорта
Как появляется круговой импорт? Эта ошибка импорта обычно возникает, когда два или более взаимозависимых модуля пытаются совершить импорт до того, как будут полностью инициализированы.
Допустим, у нас есть два модуля: module_1.py
и module_2.py
.
# module_1.py from module_2 import ModY class ModX: mody_obj = ModY()
# module_2.py from module_1 import ModX class ModY: modx_obj = ModX()
Здесь module_1
и module_2
взаимно зависят друг от друга.
Инициализация mody_obj
в module_1
зависит от module_2
, а инициализация modx_obj
в module_2
зависит от module_1
.
Это то, что мы называем круговой зависимостью. При попытке загрузить друг друга оба модуля застрянут в циклах импорта.
Если мы запустим module_1.py
, то получим следующий трейсбек:
Traceback (most recent call last): File "module_1.py", line 1, in <module> from module_2 import ModY File "module_2.py", line 1, in <module> from module_1 import ModX File "module_1.py", line 1, in <module> from module_2 import ModY ImportError: cannot import name 'ModY' from partially initialized module 'module_2' (most likely due to a circular import)
Эта ошибка объясняет ситуацию с круговым импортом. Когда программа попыталась импортировать ModY
из module_2
, в это время module_2
не был полностью инициализирован (из-за другого оператора импорта, который пытался импортировать ModX
из module_1
).
Как исправить круговой импорт в Python? Есть несколько способов.
Исправление кругового импорта в Python
Перенос кода в общий файл
Чтобы избежать ошибок импорта, можно перенести код в общий файл, а затем попытаться импортировать модули из этого файла.
# main.py ----> common file class ModX: pass class ModY: pass
В этом фрагменте кода мы перенесли классы ModX
и ModY
в общий файл (main.py
).
# module_1.py from main import ModY class Mod_X: mody_obj = ModY()
# module_2.py from main import ModX class Mod_Y: modx_obj = ModX()
Теперь module_1
и module_2
импортируют классы из main
, что исправляет ситуацию с круговым импортом.
У этого подхода есть проблема: иногда кодовая база настолько велика, что переносить код в другой файл становится рискованно.
Перенос импорта в конец модуля
Мы можем перенести оператор import
в конец модуля. Это даст время на полную инициализацию модуля перед импортом другого модуля.
# module_1.py class ModX: pass from module_2 import ModY class Mod_X: mody_obj = ModY()
# module_2.py class ModY: pass from module_1 import ModX
Импорт модуля в области видимости класса или функции
Кругового импорта можно избежать, импортируя модули в области видимости класса или функции. Это позволяет импортировать модуль только при вызове этого класса (функции). Это актуально, когда мы хотим минимизировать использование памяти.
# module_1.py class ModX: pass class Mod_X: from module_2 import ModY mody_obj = ModY()
# module_2.py class ModY: pass class Mod_Y: from module_1 import ModX modx_obj = ModX()
Мы перенесли операторы импорта в область видимости классов Mod_X
и Mod_Y
в module_1
и module_2
соответственно.
Если мы запустим module_1
или module_2
, то не получим ошибку кругового импорта. Однако при таком подходе класс становится доступным только в пределах области видимости класса, поэтому мы не можем использовать импорт глобально.
Использование имени модуля или алиаса
Использование имени модуля или просто псевдонима тоже решает проблему. Это позволяет обоим модулям загружаться полностью, откладывая круговую зависимость до времени выполнения.
# module_1.py import module_2 as m2 class ModX: def __init__(self): self.mody_obj = m2.ModY()
# module_2.py import module_1 as m1 class ModY: def __init__(self): self.modx_obj = m1.ModX()
Использование библиотеки importlib
Мы также можем использовать библиотеку importlib, чтобы импортировать модули динамически.
# module_1.py import importlib class ModX: def __init__(self): m2 = importlib.import_module('module_2') self.mody_obj = m2.ModY()
# module_2.py import importlib class ModY: def __init__(self): m1 = importlib.import_module('module_1') self.mody_obj = m1.ModX()
Круговой импорт в пакетах Python
Обычно круговой импорт происходит в модулях одного пакета. В сложных проектах структура каталогов также сложная, с пакетами внутри пакетов.
Эти пакеты и подпакеты содержат файлы __init__.py
, чтобы обеспечить более легкий доступ к модулям. Именно здесь иногда непреднамеренно возникают круговые зависимости между модулями.
Допустим, у нас есть следующая структура каталогов:
root_dir/ |- mainpkg/ |---- modpkg_x/ |-------- __init__.py |-------- module_1.py |-------- module_1_1.py |---- modpkg_y/ |-------- __init__.py |-------- module_2.py |---- __init__.py |- main.py
У нас есть пакет mainpkg
и файл main.py
. Внутри mainpkg
есть два подпакета: modpkg_x
и modpkg_y
.
Вот как выглядит каждый Python-файл в modpkg_x
и modpkg_y
.
mainpkg/modpkg_x/__init__.py:
from .module_1 import ModX from .module_1_1 import ModA
Этот файл импортирует оба класса (ModX
и ModA
) из module_1
и module_1_1
.
mainpkg/modpkg_x/module_1.py:
from ..modpkg_y.module_2 import ModY class ModX: mody_obj = ModY()
module_1
импортирует класс ModY
из module_2
.
mainpkg/modpkg_x/module_1_1.py:
class ModA: pass
module_1_1
ничего не импортирует. Он не зависит ни от одного модуля.
mainpkg/modpkg_y/__init__.py:
from .module_2 import ModY
Этот файл импортирует класс ModY
из module_2
.
mainpkg/modpkg_y/module_2.py:
from ..modpkg_x.module_1_1 import ModA class ModY: moda_obj = ModA()
module_2
импортирует класс ModA
из module_1_1
.
В файле main.py
мы имеем следующий код.
root_dir/main.py:
from mainpkg.modpkg_y.module_2 import ModY def mody(): y_obj = ModY() mody()
Файл main
импортирует класс ModY
из module_2
. Этот файл является зависимым от module_2
.
Если не учитывать файлы __init__.py
внутри modpkg_x
и modpkg_y
, цикл импорта будет выглядеть следующим образом:
Мы видим, что файл main
зависит от module_2
, module_1
также зависит от module_2
, а module_2
зависит от module_1_1
. Кругового импорта нет.
Но вы знаете, что модули зависят от своего файла __init__.py
, поэтому сначала инициализируется файл __init__.py
, а затем модули повторно импортируются.
Вот как теперь выглядит цикл импорта.
В результате module_1_1
стал зависеть от module_1
, что является фальшивой зависимостью.
Если дело обстоит именно так, то удаление файлов __init__.py
из подпакетов и использование отдельного файла __init__.py
может помочь централизовать импорт на уровне пакета.
root_dir/ |- mainpkg/ |---- modpkg_x/ |-------- __init__.py # empty file |-------- module_1.py |-------- module_1_1.py |---- modpkg_y/ |-------- __init__.py # empty file |-------- module_2.py |---- subpkg/ |-------- __init__.py |---- __init__.py |- main.py
В этой структуре мы добавили еще один подпакет subpkg
внутри mainpkg
.
mainpkg/subpkg/__init__.py:
from ..modpkg_x.module_1 import ModX from ..modpkg_x.module_1_1 import ModA from ..modpkg_y.module_2 import ModY
Это позволит внутренним модулям импортировать из одного источника, что уменьшит необходимость в перекрестном импорте.
Теперь можно обновить оператор import
в файле main.py
.
root_dir/main.py:
from mainpkg.subpkg import ModY def mody(): y_obj = ModY() mody()
Это решит проблему круговой зависимости между модулями внутри одного пакета.
Заключение
Круговая зависимость или круговой импорт в Python — это запах кода, который указывает на необходимость серьезной реструктуризации и рефакторинга кода.
Чтобы избежать круговой зависимости в Python, вы можете попробовать любой из вышеупомянутых способов.
Перевод статьи “Stuck with circular imports? Use these methods to avoid them…”.