Что такое круговая зависимость?
Циклическая зависимость возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого (см. Рисунок 1).
Например:
functionA():
functionB()
А также
functionB():
functionA()
Приведенный выше код демонстрирует довольно очевидную циклическую
зависимость. functionA()
вызывает functionB()
, следовательно, в
зависимости от него, а functionB()
вызывает functionA()
. У этого
типа циклической зависимости есть некоторые очевидные проблемы, которые
мы опишем немного дальше в следующем разделе.
{.ezlazyload .img-responsive}
фигура 1
Проблемы с круговыми зависимостями
Циклические зависимости могут вызвать множество проблем в вашем коде. Например, это может привести к тесной связи между модулями и, как следствие, к снижению возможности повторного использования кода. Этот факт также усложняет сопровождение кода в долгосрочной перспективе.
Кроме того, циклические зависимости могут быть источником потенциальных сбоев, таких как бесконечные рекурсии, утечки памяти и каскадные эффекты. Если вы не будете осторожны и в вашем коде будет циклическая зависимость, может быть очень сложно отладить множество потенциальных проблем, которые он вызывает.
Что такое циклический импорт?
Циклический импорт - это форма циклической зависимости, которая создается с помощью оператора импорта в 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()
пытается вызвать 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 только при необходимости.
Этот подход не противоречит синтаксису Python, поскольку документация Python гласит : «Обычно, но не обязательно размещать все операторы импорта в начале модуля (или сценария, если на то пошло)».
В документации Python также говорится, что рекомендуется использовать
import X
вместо других операторов, таких как from module import *
или from module import a,b,c
.
Вы также можете увидеть много кодовых баз, использующих отложенный импорт, даже если нет циклической зависимости, которая ускоряет время запуска, поэтому это вообще не считается плохой практикой (хотя это может быть плохой дизайн, в зависимости от вашего проекта) .
Подведение итогов
Циклический импорт - это особый случай циклических ссылок. Как правило, их можно решить с помощью лучшего дизайна кода. Однако иногда результирующий дизайн может содержать большой объем кода или смешивать несвязанные функции (тесная связь).
Вы сталкивались с циклическим импортом в собственном коде? Если да, то как вы это исправили? Дайте нам знать об этом в комментариях!