Является ли python циркулярный импорт деталью реализации? - PullRequest
3 голосов
/ 12 октября 2019

Правильно ли задано поведение циклического импорта в Python или оно зависит только от реализации?

Например, при импорте подмодуля в cpython подмодуль назначается какатрибут родительского модуля, , но только после выполнение субмодуля. Указано ли это где-нибудь в документах или PEP?

Чаще всего циклический импорт python просто работает (чему помогают соглашения, такие как не выполнять какие-либо функции во время импорта, и выкладывать исходный код для определения функций в обратном порядке). ожидаемого порядка вызовов), но следствием вышеприведенного поведения является то, что вы не можете ссылаться через родителя на частично импортированный модуль. Например, если импорт package.submodule вызывает импорт othermodule, тогда othermodule не может сделать import package.submodule as submod, но может сделать from package import submodule as submod. Это объяснено / указано в документации? Если нет, то вероятно ли это изменить?

В этом случае расхождение связано с тем, что import package.submodule as submod реализован как вызов __import__ (который возвращает package) с последующим поиском атрибута (для извлеченияsubmodule, чтобы его можно было присвоить submod). Напротив, from package import submodule as submod заставляет __import__ напрямую возвращать submodule (что, кажется, работает, даже если submodule все еще только наполовину завершен). Вы можете разобраться с этим, если изучите байт-код cpython (задокументированный в разделе dis стандартной библиотеки) и изучите семантику __import__. Тем не менее, поскольку разрешение кругового импорта является распространенной проблемой в python, было бы полезно найти официальную сводную информацию о том, чего ожидать?

Пример:

mkdir package
touch package/__init__.py
cat > package/submodule.py << EOF
def f():
  print('OK')
import othermodule
othermodule.g()
def notyetdefined():
  pass
EOF
cat > othermodule.py << EOF
def g():
  try:
    import package.submodule as submod # this fails
  except AttributeError:
    print('FAIL')
  from package import submodule as submod # this works ok
  submod.f()
  #submod.notyetdefined() # this cannot be invoked
EOF
python -c 'import package.submodule'

Вывод в python3.6.7:

FAIL
OK

1 Ответ

1 голос
/ 12 октября 2019

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

Хотя я не исследовал правила системы импорта Python очень подробно, я смог разобраться в конкретной проблеме, которую вы наблюдали.

Чтобы добраться до нее, я впервые заметил, что поведениеваш код изменился в Python 3.7: теперь он печатает только OK. Эта точка из журнала изменений для Python 3.7 говорит, почему:

Круговой импорт, включающий абсолютный импорт с привязкой подмодуля к имени, теперь поддерживается. (Внесено Сергеем Сторчака в bpo-30024 .)

Дискуссия по проблеме Python, которая привела к изменению, содержит некоторое обсуждение того, что происходило раньше, а такженесколько ссылок на предыдущие обсуждения того, почему поведение до 3.7 работало, почему оно работало. Я нашел несколько комментариев и ссылок, которые были бы особенно полезны здесь:

Самый первый комментарий дает объяснение поведения, которое вы наблюдали ( этот ответ переполнения стека обсуждает, как та же ошибкапроисходит в аналогичном случае):

Фон здесь - это изменение в http://bugs.python.org/issue17636, которое позволяет IMPORT_FROM возвращаться к sys.modules, когда записывается как «из ab import c как m»в то время как обычный LOAD_ATTR, сгенерированный для «import abc as m», завершается неудачей.

Обратите внимание, что bpo-17636 мотивировал поддержку «[c] циклического импорта, включающего относительный импорт» в Python 3.5 . Здесь моя общая идея о

В более общем смысле, для вашего ответа этот комментарий (от самого Гвидо) гласит, что поведение кругового импорта определено :

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

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

(Обратите внимание, что в документации для системы импорта также не упоминаются изменения в 3.7 и не упоминаютсячто-нибудь о import ... as ... или from ... import ... вообще. Пока документация для оператора import делает ошибкуПоскольку различные формы утверждения, он также не обсуждает циклы (и большая часть его не обновлялась за как минимум 3 года ).)


Также естьодно небольшое изменение, появившееся в Python 3.8, хотя оно, похоже, отсутствует в в журнале изменений .

Если вы раскомментируете submod.notyetdefined() в othermodule, Python 3.6 и 3.7 вызовут следующееошибка:

AttributeError: модуль 'package.submodule' не имеет атрибута 'notyetdefined'

В Python 3.8.0b4 вместо этого создается более полезное сообщение:

AttributeError: частично инициализированный модуль 'package.submodule' не имеет атрибута 'notyetdefined' (скорее всего, из-за циклического импорта)

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


1 Как ни странно, эта цитата взята из этого письма , которое приводится в обсуждении для bpo-30024 в качестве обоснования изменений, произошедших от bpo-17636 - они исправили однуслучай, но оставил еще один, как это было, и это стало причиной вашей ошибки.

...