Когда Python циклический импорт неизбежен, что является лучшим решением? - PullRequest
0 голосов
/ 11 февраля 2019

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

У нас есть следующая структура файла:

|- pack
|  |- sub_a
|  |  |- __init__.py
|  |  |- a_func.py
|  |- sub_b
|  |  |- __init__.py
|  |  |- b_func.py
|  |- __init__.py
|- runit.py

Основная программа - runit.py, этоимпортирует определенные файлы из пакета pack и запускает его.

runit.py:

import pack

print(pack.fa())
print(pack.fb())
print(pack.fab())
print(pack.fba())

Пакет пакета состоит из остальных файлов, начиная с pack\__init__.py:

from .sub_a import fa, fab
from .sub_b import fb, fba

pack\sub_a\__init__.py:

from .a_func import fa, fab

pack\sub_a\a_func.py:

from ..sub_b import fb


def fa():
    return 0


def fab():
    return fb()

pack\sub_b\__init__.py:

from .b_func import fb, fba

pack\sub_b\b_func.py:

from ..sub_a import fa


def fb():
    return 1


def fba():
    return fa()

Теперь вот проблема: если вы попытаетесь запустить runit.py, он завершится неудачно с:

Traceback (most recent call last):
  File "C:/adir/runit.py", line 1, in <module>
    import pack
  File "C:\adir\pack\__init__.py", line 1, in <module>
    from .sub_a import fa, fab
  File "C:\adir\pack\sub_a\a_func.py", line 1, in <module>
    from ..sub_b import fb
  File "C:\adir\pack\sub_b\__init__.py", line 1, in <module>
    from .b_func import fb, fba
  File "C:\adir\pack\sub_b\b_func.py", line 1, in <module>
    from ..sub_a import fa
ImportError: cannot import name 'fb' 'fa' from 'pack.sub_a' (C:\adir\pack\sub_a\__init__.py)

Однако, если вы удалите импорт fa(и измените строку, используя ее в fba()), она работает, несмотря на одинаковый импорт fb в a_func.py.

Причина в том, что sub_a нужно что-то отsub_b, но sub_b нужно что-то из sub_a, поэтому создается циклический импорт.

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

Но для реальной ситуации есть веская причина для некоторой циклической ссылки (я говорю «несколько», потому что нет фактической циклической ссылки).зависимость функций, но, конечно, Python не знает об этом до времени выполнения).

Единственные варианты, которые я вижу:

  1. - все помещается в одно место;
  2. реорганизовать модули так, чтобы код, который требуется всем остальным, находился в отдельном модуле, который не зависит от остальных;в этом примере это будет fa() и / или fb();
  3. , чтобы переместить оператор импорта туда, где он необходим, а не вверху;в этом примере он будет перемещен внутрь определения fba();
  4. реорганизует фактические функции / классы, полагаясь на внедрение зависимостей;в этом примере fba() может иметь параметр callback;
  5. добавить метод инициализации для модуля.

Мне не нравятся первые четыре: опция 1.становится болью, чтобы читать содержание.Вариант 2. означает, что код, который в настоящее время довольно хорошо написан и имеет большой смысл, должен быть разбит на две или более частей.Вариант 3. кажется очень похожим на анти-шаблон, так как из-за этого трудно увидеть, какие зависимости есть у любого модуля.А в варианте 4. соглашения о вызовах для существующих функций меняются по всем направлениям.

Примечание: изначально я беспокоился о варианте 3. что любой код в теле модуля будет выполняться при каждом импорте;однако @a_guest указал, что «модули кэшируются (в sys.modules) и будут извлекаться из этого кэша при необходимости, без повторного выполнения кода модуля"

Я склонен использовать опцию5. в этом случае и переписать pack\sub_b\b_func.py примерно так:

def initialise():
    global fa
    from pack import fa as pack_fa
    fa = pack_fa


def fb():
    return 0


def fba():
    return fa()

Обновление pack\sub_b\__init__.py:

from .b_func import fb, fba, initialise

А затем pack\__init__.py становится:

from .sub_a import fa, fab
from .sub_b import fb, fba

for module in [sub_a, sub_b]:
    if hasattr(module, 'initialise'):
        module.initialise()

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

Недостатком является то, что__init__.py пакета теперь знает о необходимости инициализации модуля sub_b, и этот модуль сам имеет некоторый дополнительный код, который есть только в качестве обходного пути к этой проблеме, но весь другой код работает точно так, как ожидается (обановое и наследие).

И до тех пор, пока кто-либо, импортирующий sub_b, будет вызывать initialise хотя бы один раз, прежде чем использовать его содержимое, он может все еще использоваться за пределами pack, при условии наличия правильных зависимостей.

Мой вопрос: Есть ли шестой, возможно, даже лучший вариант для решения проблемы кругового импорта?

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

Отказ от ответственности: мне известны другие вопросы о SOциклические зависимости и циклические импорты в Python, но я не думаю, что мой вопрос дублирует их, поскольку ни один из них не проникает так глубоко в лес и задает последний вопрос

...