объяснение
От PEP 328
Относительный импорт использует атрибут модуля __name__, чтобы определить позицию этого модуля в иерархии пакетов. Если имя модуля не содержит никакой информации о пакете (например, для него задано значение «__main__»),
то относительный импорт разрешается так, как если бы модуль был модулем верхнего уровня , независимо от того, где этот модуль фактически расположен в файловой системе.
В какой-то момент PEP 338 вступил в конфликт с PEP 328 :
... относительный импорт полагается на __name__, чтобы определить текущую позицию модуля в иерархии пакетов. В главном модуле значение __name__ всегда равно __main__ , поэтому явный относительный импорт всегда будет неудачным (так как он работает только для модуля внутри пакета)
и для решения этой проблемы PEP 366 представил переменную верхнего уровня __package__
:
Добавляя новый атрибут уровня модуля, этот PEP позволяет относительному импорту работать автоматически, если модуль выполняется с помощью
ключа -m . Небольшое количество стандартного шаблона в самом модуле позволит относительному импорту работать, когда файл выполняется по имени. [...] Когда он [атрибут] присутствует, относительный импорт будет основываться на этом атрибуте, а не на атрибуте __name__ модуля . [...] Если основной модуль указан в имени файла, атрибуту __package__ будет присвоено значение Нет . [...] Когда система импорта обнаруживает явный относительный импорт в модуле без установленного __package__ (или с установленным на None), она вычислит и сохранит правильное значение (__name __. rpartition ('.') [0] для обычных модулей и __name__ для модулей инициализации пакетов)
(акцент мой)
Если __name__
есть '__main__'
, __name__.rpartition('.')[0]
возвращает пустую строку. Вот почему в описании ошибки есть пустой строковый литерал:
SystemError: Parent module '' not loaded, cannot perform relative import
Соответствующая часть в CPython в PyImport_ImportModuleLevelObject
функции :
if (PyDict_GetItem(interp->modules, package) == NULL) {
PyErr_Format(PyExc_SystemError,
"Parent module %R not loaded, cannot perform relative "
"import", package);
goto error;
}
CPython вызывает это исключение, если не удалось найти package
(имя пакета) в interp->modules
(доступно как sys.modules
). Поскольку sys.modules
это «словарь, который отображает имена модулей на модули, которые уже были загружены» , теперь ясно, что родительский модуль должен быть явно импортирован абсолютно перед выполнением относительного импорта .
Примечание: патч из выпуска 18018 добавил еще один if
блок , который будет выполнен перед кодом выше:
if (PyUnicode_CompareWithASCIIString(package, "") == 0) {
PyErr_SetString(PyExc_ImportError,
"attempted relative import with no known parent package");
goto error;
} /* else if (PyDict_GetItem(interp->modules, package) == NULL) {
...
*/
Если package
(как указано выше) пустая строка, сообщение об ошибке будет
ImportError: attempted relative import with no known parent package
Тем не менее, вы увидите это только в Python 3.6 или новее.
Решение № 1: Запустите ваш скрипт, используя -m
Рассмотрим каталог (который является пакетом Python ):
.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
Все файлы в пакете начинаются с одинаковых двух строк кода:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
Я включил эти две строки только для того, чтобы сделать порядок операций очевидным. Мы можем полностью их игнорировать, поскольку они не влияют на исполнение.
__init__.py и module.py содержат только эти две строки (т.е. они фактически пусты).
standalone.py дополнительно пытается импортировать module.py через относительный импорт:
from . import module # explicit relative import
Мы прекрасно понимаем, что не /path/to/python/interpreter package/standalone.py
получится. Однако мы можем запустить модуль с параметром -m
командной строки, который будет «искать sys.path
указанный модуль и выполнять его содержимое как __main__
модуль» :
vaultah@base:~$ python3 -i -m package.standalone
Importing /home/vaultah/package/__init__.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/module.py
>>> __file__
'/home/vaultah/package/standalone.py'
>>> __package__
'package'
>>> # The __package__ has been correctly set and module.py has been imported.
... # What's inside sys.modules?
... import sys
>>> sys.modules['__main__']
<module 'package.standalone' from '/home/vaultah/package/standalone.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
-m
выполняет все операции импорта для вас и автоматически устанавливает __package__
, но вы можете сделать это самостоятельно в
Решение № 2: Установите __package__ вручную
Пожалуйста, рассматривайте это как доказательство концепции, а не фактическое решение. Он не очень подходит для использования в реальном коде.
В PEP 366 есть обходной путь для этой проблемы, однако он неполон, поскольку __package__
одной настройки недостаточно. Вам нужно будет импортировать как минимум N предыдущих пакетов в иерархии модулей, где N - это количество родительских каталогов (относительно каталога скрипта), в которых будет выполняться поиск импортируемого модуля.
Таким образом,
Добавьте родительский каталог N-го предшественника текущего модуля вsys.path
Удалить каталог текущего файла из sys.path
Импортировать родительский модуль текущего модуля, используя его полное имя
Установите __package__
полное имя из 2
Выполните относительный импорт
Я позаимствую файлы из Решения № 1 и добавлю еще несколько подпакетов:
package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
На этот раз standalone.py импортирует module.py из пакета , используя следующий относительный импорт
from ... import module # N = 3
Нам нужно будет поставить перед этой строкой стандартный код, чтобы она заработала.
import sys
from pathlib import Path
if __name__ == '__main__' and __package__ is None:
file = Path(__file__).resolve()
parent, top = file.parent, file.parents[3]
sys.path.append(str(top))
try:
sys.path.remove(str(parent))
except ValueError: # Already removed
pass
import package.subpackage.subsubpackage
__package__ = 'package.subpackage.subsubpackage'
from ... import module # N = 3
Это позволяет нам выполнять standalone.py по имени файла:
vaultah@base:~$ python3 package/subpackage/subsubpackage/standalone.py
Running /home/vaultah/package/subpackage/subsubpackage/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/subpackage/__init__.py
Importing /home/vaultah/package/subpackage/subsubpackage/__init__.py
Importing /home/vaultah/package/module.py
Более общее решение, заключенное в функцию, можно найти здесь . Пример использования:
if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
Шаги -
Заменить явный относительный импорт эквивалентным абсолютным импортом
Установите, package
чтобы сделать его импортируемым
Например, структура каталога может быть следующей
.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
где setup.py является
from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
Остальные файлы были заимствованы из Решения № 1 .
Установка позволит вам импортировать пакет независимо от вашего рабочего каталога (при условии, что проблем с именами не будет).
Мы можем изменить standalone.py, чтобы использовать это преимущество (шаг 1):
from package import module # absolute import
Измените ваш рабочий каталог на project
и запустите /path/to/python/interpreter setup.py install --user
( --user
устанавливает пакет в ваш каталог site-packages ) (шаг 2):
vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user
Давайте проверим, что теперь можно запускать standalone.py как скрипт:
vaultah@base:~/project$ python3 -i package/standalone.py
Running /home/vaultah/project/package/standalone.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
Примечание . Если вы решите пойти по этому пути, вам лучше использовать виртуальные среды для установки пакетов изолированно.
Решение № 4: Используйте абсолютный импорт и некоторый шаблонный код
Честно говоря, установка не обязательна - вы можете добавить шаблонный код в ваш скрипт, чтобы обеспечить абсолютный импорт.
Я собираюсь позаимствовать файлы из решения № 1 и изменить standalone.py :
Добавить родительский каталог пакета , чтобы sys.path
прежде , чем пытаться что - либо импорт из пакета с использованием абсолютного импорта:
import sys
from pathlib import Path # if you haven't already done so
file = Path(__file__).resolve()
parent, root = file.parent, file.parents[1]
sys.path.append(str(root))
# Additionally remove the current file's directory from sys.path
try:
sys.path.remove(str(parent))
except ValueError: # Already removed
pass
Замените относительный импорт на абсолютный импорт:
from package import module # absolute import
standalone.py работает без проблем:
vaultah@base:~$ python3 -i package/standalone.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
Я чувствую, что должен предупредить вас: старайтесь не делать этого, особенно если ваш проект имеет сложную структуру.
В качестве примечания, PEP 8 рекомендует использовать абсолютный импорт, но утверждает, что в некоторых сценариях допустим явный относительный импорт:
Рекомендуется абсолютный импорт, так как они обычно более читабельны и, как правило, ведут себя лучше (или, по крайней мере, дают лучшие сообщения об ошибках). [...] Однако явный относительный импорт является приемлемой альтернативой абсолютному импорту, особенно когда речь идет о сложных макетах пакетов, где использование абсолютного импорта было бы излишне многословным.