Циклический импорт в Python
Циклическая зависимость в Python возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого.
Например:
functionA():
functionB()
functionB():
functionA()
Приведенный выше код демонстрирует довольно очевидную циклическую зависимость. functionA() вызывает functionB(), следовательно, в зависимости от него, а functionB() вызывает functionA(). У этого типа циклической зависимости есть некоторые очевидные проблемы, которые мы опишем немного дальше в следующем разделе.
Проблемы с круговыми зависимостями
Циклические зависимости могут вызвать множество проблем в вашем коде. Например, это может привести к тесной связи между модулями и, как следствие, к снижению возможности повторного использования кода. Этот факт также усложняет сопровождение кода в долгосрочной перспективе.
Кроме того, циклические зависимости могут быть источником потенциальных сбоев, таких как бесконечные рекурсии, утечки памяти и каскадные эффекты. Если вы не будете осторожны и у вас есть циклическая зависимость в вашем коде, может быть очень сложно отладить множество потенциальных проблем, которые он вызывает.
Что такое циклический импорт в Python?
Циклический импорт в Python – это форма циклической зависимости, которая создается с помощью оператора импорта в Python.
Например, давайте проанализируем следующий код:
# module1
import module2
def function1():
module2.function2()
def function3():
print('Goodbye, World!')
# module2
import module1
def function2():
print('Hello, World!')
module1.function3()
# __init__.py import module1 module1.function1()
Когда Python импортирует модуль, он проверяет реестр модулей, чтобы убедиться, что он уже импортирован. Если модуль уже был зарегистрирован, Python использует этот существующий объект из кеша. Реестр модулей – это таблица модулей, которые были инициализированы и проиндексированы по имени модуля. К этой таблице можно получить доступ через sys.modules.
Если он не был зарегистрирован, Python находит модуль, при необходимости инициализирует его и выполняет в пространстве имен нового модуля.
В нашем примере, когда Python достигает import module2, он загружает и выполняет его. Однако module2 также вызывает module1, который, в свою очередь, определяет function1().
Проблема возникает, когда function2() пытается вызвать функцию module1 function3(). Поскольку модуль1 был загружен первым и, в свою очередь, загружен модуль2 до того, как он смог достичь function3(), эта функция еще не определена и выдает ошибку при вызове:
$ python __init__.py
Hello, World!
Traceback (most recent call last):
File "__init__.py", line 3, in <module>
module1.function1()
File "/Users/scott/projects/sandbox/python/circular-dep-test/module1/__init__.py", line 5, in function1
module2.function2()
File "/Users/scott/projects/sandbox/python/circular-dep-test/module2/__init__.py", line 6, in function2
module1.function3()
AttributeError: 'module' object has no attribute 'function3'
Как исправить?
Как правило, циклический импорт – это результат плохого дизайна. Более глубокий анализ программы мог бы сделать вывод, что зависимость на самом деле не требуется или что зависимые функции могут быть перемещены в другие модули, которые не будут содержать циклическую ссылку.
Простое решение состоит в том, что иногда оба модуля можно просто объединить в один более крупный. Результирующий код из нашего примера выше будет выглядеть примерно так:
# module 1 2
def function1():
function2()
def function2():
print('Hello, World!')
function3()
def function3():
print('Goodbye, World!')
function1()
Однако объединенный модуль может иметь некоторые несвязанные функции (тесная связь) и может стать очень большим, если в двух модулях уже есть много кода.
Так что, если это не сработает, можно было бы отложить импорт module2, чтобы импортировать его только тогда, когда это необходимо. Это можно сделать, поместив импорт module2 в определение function1():
# module 1
def function1():
import module2
module2.function2()
def function3():
print('Goodbye, World!')
В этом случае Python сможет загрузить все функции в module1, а затем загрузить module2 только при необходимости.
Этот подход не противоречит синтаксису, поскольку в документации сказано: «Обычно, но не обязательно, помещать все операторы импорта в начало модуля».
В документации также говорится, что рекомендуется использовать import X вместо других операторов, таких как from module import * или from module import a, b, c.
Вы также можете увидеть много кодовых баз, использующих отложенный импорт, даже если нет циклической зависимости, которая ускоряет время запуска, поэтому это вообще не считается плохой практикой (хотя это может быть плохой дизайн, в зависимости от вашего проекта) .
Заключение
Циклический импорт – это особый случай циклических ссылок. Как правило, их можно решить с помощью улучшенного дизайна кода. Однако иногда результирующий дизайн может содержать большой объем кода или смешивать несвязанные функции (жесткая связь).
Автор