Такое поведение является ошибкой в cpython.
Каждый оператор python import
транслируется в один или несколько вызовов встроенной функции __import__
python. (Это задокументировано и может быть перехвачено.)
В cpython есть две реализации __import__
: есть реализация Python reference (в стандартной библиотеке importlib
), и есть реализация C (к которой можно получить доступ или перехватить через builtins
(стандартная библиотека), которая вызывается по умолчанию.
Вот скрипт, который исследует проблему (примечание curses.ascii
и curses.textpad
- некоторые модули в стандартной библиотеке Python):
commands = ['from curses import ascii',
'from . import ascii',
'from . import textpad']
def mock(name, globals=None, locals=None, fromlist=(), level=0):
print(' __import__ :', repr(name), ':', fromlist, ':', level)
return alternate(name, globals, locals, fromlist, level)
import builtins
import importlib._bootstrap
original = builtins.__import__
builtins.__import__ = mock
for implementation in ['original', 'importlib._bootstrap.__import__']:
print(implementation.upper(), '\n')
alternate = eval(implementation)
try:
for command in commands:
print(command)
exec(command)
except ImportError as err:
print(' ', repr(err), '\n\n')
Вывод демонстрирует, что встроенный в cpython, в отличие от эталонной реализации, не может проверить родительский пакет перед попыткой относительного импорта:
ORIGINAL
from curses import ascii
__import__ : 'curses' : ('ascii',) : 0
__import__ : '_curses' : ('*',) : 0
__import__ : 'os' : None : 0
__import__ : 'sys' : None : 0
from . import ascii
__import__ : '' : ('ascii',) : 1
from . import textpad
__import__ : '' : ('textpad',) : 1
ImportError("cannot import name 'textpad'",)
IMPORTLIB._BOOTSTRAP.__IMPORT__
from curses import ascii
__import__ : 'curses' : ('ascii',) : 0
from . import ascii
__import__ : '' : ('ascii',) : 1
ImportError('attempted relative import with no known parent package',)
В cpython оператор from [...][X] import Y [as Z]
преобразуется в две главные инструкции байт-кода (плюс некоторые служебные инструкции для соответствующей загрузки и сохранения между стеком и списками констант / переменных):
IMPORT_NAME
: выполняется вызов builtins.__import__
. Аргументами вызова являются аргумент инструкции (имя X
возвращаемого модуля), некоторое текущее состояние кадра интерпретатора (globals()
и locals()
) и два элемента, извлеченные из стека (список Y
который может содержать субмодули для импорта, и относительный уровень, то есть число [...]
). Ожидается, что вызов вернет объект модуля, который помещен в стек.
IMPORT_FROM
: Это проверяет модуль на вершине стека и получает от его атрибута Y
объект (который он также оставляет в стеке).
(Они документированы вместе с библиотекой dis
и реализованы в ceval.c
.)
Если мы попытаемся from . import foo
(т. Е. X
пусто, а уровень равен 1), тогда IMPORT_NAME
попытается вернуть объект модуля для текущего родительского пакета (например, имя, которое указано глобальным __package__
) , Если у него нет атрибута с именем foo
, то IMPORT_FROM
поднимает ImportError
.
В интерактивной оболочке интерпретатора или в простом скрипте __package__
равен None
. При таких обстоятельствах:
importlib.__import__
вызвало бы ImportError
(попытка относительного импорта без известного родительского пакета), но
builtins.__import__
возвращает модуль __main__
(встроенный), представляющий собой среду сценариев верхнего уровня python.
Это ключевое отличие. Поскольку все глобальные переменные являются атрибутами модуля __main__
, это неправильное поведение приводит к:
>>> foo = 'oops'
>>> from . import foo as fubar
>>> fubar
'oops'
Существует также еще одно неправильное поведение: при попытке выполнить более глубокий уровень относительного импорта (помимо пакета верхнего уровня, например, from ..... import foo
), тогда builtins.__import__
поднимает ValueError
(вместо ожидаемого ImportError
).