Рубрики
Без рубрики

Круговой импорт Python

Автор оригинала: Scott Robinson.

Круговой импорт Python

Что такое циклическая зависимость?

Циклическая зависимость возникает, когда два или более модуля зависят друг от друга. Это связано с тем, что каждый модуль определяется в терминах другого (см. рис. 1).

Например:

functionA():
    functionB()

И

functionB():
    functionA()

Приведенный выше код изображает довольно очевидную циклическую зависимость. functionA() вызывает functionB() , таким образом, в зависимости от него, и functionB() вызывает functionA() . Этот тип циклической зависимости имеет некоторые очевидные проблемы, которые мы опишем немного дальше в следующем разделе.

Рисунок 1

Проблемы с циклическими зависимостями

Циклические зависимости могут вызвать довольно много проблем в вашем коде. Например, это может привести к возникновению тесной связи между модулями и, как следствие, к снижению возможности повторного использования кода. Этот факт также делает код более трудным для поддержания в долгосрочной перспективе.

Кроме того, циклические зависимости могут быть источником потенциальных сбоев, таких как бесконечные рекурсии, утечки памяти и каскадные эффекты. Если вы не будете осторожны и у вас есть циклическая зависимость в вашем коде, может быть очень трудно отладить многие потенциальные проблемы, которые она вызывает.

Что такое круговой импорт?

Циклический импорт-это форма циклической зависимости, которая создается с помощью оператора import в 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 , он загружает и выполняет его. Однако модуль 2 также вызывает модуль 1, который, в свою очередь, определяет function1() .

Проблема возникает, когда функция 2() пытается вызвать модуль 1 функция 3() . Поскольку модуль 1 был загружен первым и, в свою очередь , loadedmodule 2 до того, как он смог достичь функции 3 () , эта функция еще не определена и выдает ошибку при вызове:

$ python __init__.py
Hello, World!
Traceback (most recent call last):
  File "__init__.py", line 3, in 
    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()

Однако объединенный модуль может иметь некоторые несвязанные функции (тесная связь) и может стать очень большим, если в двух модулях уже есть много кода.

Поэтому, если это не сработает, другим решением могло бы быть отложить импорт модуля 2, чтобы импортировать его только тогда, когда это необходимо. Это можно сделать, поместив импорт модуля 2 в определение функции 1() :

# module 1

def function1():
    import module2
    module2.function2()

def function3():
    print('Goodbye, World!')

В этом случае Python сможет загрузить все функции в модуле 1, а затем загрузить модуль 2 только при необходимости.

Этот подход не противоречит синтаксису Python, как говорится в документации Python : “Обычно, но не обязательно размещать все операторы импорта в начале модуля (или скрипта, если уж на то пошло)”.

В документации Python также говорится , что рекомендуется использовать import X вместо других операторов, таких как from module import * или from module import a , b,c .

Вы также можете увидеть много кодовых баз, использующих отложенный импорт, даже если нет циклической зависимости, которая ускоряет время запуска, так что это вообще не считается плохой практикой (хотя это может быть плохой дизайн, в зависимости от вашего проекта).

Сворачивание

Циклический импорт-это особый случай циклических ссылок. Как правило, они могут быть решены с помощью лучшего дизайна кода. Однако иногда результирующая конструкция может содержать большое количество кода или смешивать несвязанные функциональные возможности (тесная связь).

Вы сталкивались с циклическим импортом в своем собственном коде? Если да, то как вы это исправили? Дайте нам знать в комментариях!