Команда Python for Devs подготовила перевод статьи о том, как справляться с циклическими импортами в Python. В статье показан простой приём: иногда не нужно переписывать архитектуру, а достаточно изменить стиль импорта, чтобы избежать ошибок.
Циклические импорты в Python могут запутать. Иногда достаточно просто изменить форму импорта, чтобы устранить проблему.
В Python циклический импорт возникает, когда два файла пытаются импортировать друг друга. В результате один из модулей оказывается не до конца инициализирован, и всё ломается. Лучший способ исправить такую ситуацию — организовать код слоями, чтобы зависимости при импортах шли только в одну сторону. Но иногда помогает просто поменять стиль import
. Сейчас покажу.
Предположим, у нас есть такие файлы:
# one.py
from two import func_two
def func_one():
func_two()
# two.py
from one import func_one
def do_work():
func_one()
def func_two():
print("Hello, world!")
# main.py
from two import do_work
do_work()
Если запустить main.py, получим следующее:
% python main.py
Traceback (most recent call last):
File "main.py", line 2, in <module>
from two import do_work
File "two.py", line 2, in <module>
from one import func_one
File "one.py", line 2, in <module>
from two import func_two
ImportError: cannot import name 'func_two' from partially initialized
module 'two' (most likely due to a circular import) (two.py)
Когда Python импортирует модуль, он выполняет файл построчно. Все глобальные объекты (имена верхнего уровня — включая функции и классы) становятся атрибутами объекта модуля, который в данный момент создаётся. В two.py
на второй строке мы делаем импорт из one.py
. В этот момент модуль two
уже создан, но у него пока нет никаких атрибутов — ведь ещё ничего не определено. В нём появятся do_work
и func_two
, но до этого мы не дошли: инструкции def
ещё не выполнены, значит, функций просто не существует. Как и при вызове функции, когда срабатывает import
, начинается исполнение импортируемого файла, и до текущего файла управление не возвращается, пока импорт не завершится.
Импорт one.py
начинается, и на второй строке он пытается получить имя из модуля two
. Как мы уже говорили, модуль two
существует, но у него пока нет определённых имён. Это и вызывает ошибку.
Вместо того чтобы импортировать конкретные имена из модулей, можно импортировать сами модули целиком. Нужно лишь изменить форму импорта и то, как мы обращаемся к функциям из импортированных модулей. Вот так:
# one.py
import two # раньше: from two import func_two
def func_one():
two.func_two() # раньше: func_two()
# two.py
import one # раньше: from one import func_one
def do_work():
one.func_one() # раньше: func_one()
def func_two():
print("Hello, world!")
# main.py
from two import do_work
do_work()
Если запустить исправленный код, получится:
% python main.py
Hello, world!
Теперь всё работает, потому что в two.py
мы импортируем one на второй строке, а в one.py
— импортируем two
тоже на второй строке. Это не мешает, ведь модуль two
уже существует. Он всё ещё пустой, как и раньше, но теперь мы не пытаемся во время импорта найти в нём имя, которого ещё нет. Когда все импорты завершаются, и one, и two получают все свои определения, и мы можем спокойно обращаться к ним внутри функций.
Ключевая идея здесь в том, что конструкция from two import func_two
пытается найти func_two
во время импорта, когда этой функции ещё не существует. Перенос поиска имени в тело функции с помощью import two
позволяет всем модулям полностью инициализироваться до того, как мы попытаемся их использовать, что и устраняет ошибку циклического импорта.
Как я упоминал в начале, лучший способ решить проблему циклических импортов — это организовать код так, чтобы модули не зависели друг от друга. Но сделать это бывает непросто, и такой приём может помочь выиграть время и снова заставить код работать.
Русскоязычное сообщество про Python

Друзья! Эту статью перевела команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!