Обнаружение кругового импорта

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

Конечно, когда я добавляю циклический импорт, я не знаю об этом. Иногда довольно очевидно, что я сделал круговой импорт, когда получаю ошибку, например AttributeError: 'module' object has no attribute 'attribute', где я четко определил 'attribute'. Но в других случаях код не генерирует исключений из-за того, как он используется.

Итак, на мой вопрос:

Можно ли программно определить, когда и где происходит циклический импорт?

Единственное решение, о котором я могу думать до сих пор, - иметь модуль importTracking, содержащий dict importingModules, функцию importInProgress(file), которая увеличивает importingModules[file] и выдает ошибку, если она больше 1, и функцию importComplete(file), которая уменьшает importingModules[file]. Все остальные модули выглядели бы так:

import importTracking
importTracking.importInProgress(__file__)
#module code goes here.
importTracking.importComplete(__file__)

Но это выглядит действительно противно, должен быть лучший способ сделать это, верно?

Ответ 1

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

Для реализации я просто использовал набор модулей "в процессе импорта", что-то вроде (benjaoming edit: Вставка рабочего фрагмента, полученного из оригинала):

beingimported = set()
originalimport = __import__
def newimport(modulename, *args, **kwargs):
    if modulename in beingimported:
        print "Importing in circles", modulename, args, kwargs
        print "    Import stack trace -> ", beingimported
        # sys.exit(1) # Normally exiting is a bad idea.
    beingimported.add(modulename)
    result = originalimport(modulename, *args, **kwargs)
    if modulename in beingimported:
        beingimported.remove(modulename)
    return result
import __builtin__
__builtin__.__import__ = newimport

Ответ 2

Не все циклические импортные проблемы являются проблемой, как вы обнаружили, когда исключение не выбрано.

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

Я не вижу никаких изменений, необходимых в этой ситуации.

Пример, когда это не проблема:

a.py

import b
a = 42
def f():
  return b.b

b.py

import a
b = 42
def f():
  return a.a

Ответ 3

Циркулярный импорт в Python не похож на PHP.

Импортированные модули Python загружаются в первый раз в "обработчик" импорта и хранятся там в течение всего процесса. Этот обработчик назначает имена в локальном пространстве имен для любого импортированного из этого модуля для каждого последующего импорта. Модуль уникален, и ссылка на это имя модуля всегда будет указывать на один и тот же загруженный модуль, независимо от того, где он был импортирован.

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

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

Ответ 4

import использует __builtin__.__import__(), поэтому, если вы обезьяны, то каждый импорт будет получать изменения. Обратите внимание, что циклический импорт не обязательно проблемы.