Команда 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 и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!

Комментарии (0)