Пояснення
Від PEP 328
Відносний імпорт використовує атрибут __name__ модуля для визначення положення цього модуля в ієрархії пакетів. Якщо ім'я модуля не містить жодної інформації про пакет (наприклад, для нього встановлено значення __main__),
то відносний імпорт вирішується так, як якщо б модуль був модулем верхнього рівня , незалежно від того, де цей модуль фактично розташований у файловій системі.
У якийсь момент PEP 338 конфліктував з PEP 328 :
... відносний імпорт розраховує на __name__ для визначення поточного положення модуля в ієрархії пакетів. В основному модулі, значення __name__ завжди «__main__» , тому явний відносний імпорт завжди буде терпіти невдачі (як вони тільки для роботи модуля всередині пакету)
і щоб вирішити цю проблему, PEP 366 представив змінну верхнього рівня __package__
:
Додаючи новий атрибут рівня модуля, цей PEP дозволяє відносному імпорту працювати автоматично, якщо модуль виконується за допомогою
перемикача -m . Невелика кількість котла в самому модулі дозволить відносному імпорту працювати, коли файл виконується по імені. [...] Якщо він [атрибут] присутній, відносний імпорт буде базуватися на цьому атрибуті, а не на атрибуті модуля __name__ . [...] Коли головний модуль буде вказаний своїм ім'ям файла, то для атрибута __package__ буде встановлено значення None . [...] Коли система імпорту стикається з явним відносним імпортом у модулі без набору __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
Рішення №3: Використовуйте абсолютний імпорт та налаштування
Етапи -
Замініть явний відносний імпорт на еквівалентний абсолютний імпорт
Встановіть, 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
встановіть пакунок у каталозі пакунків вашого сайту ) (крок 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 рекомендує використовувати абсолютний імпорт, але зазначає, що в деяких сценаріях явний відносний імпорт є прийнятним:
Рекомендований абсолютний імпорт, оскільки він, як правило, більш читабельний і має тенденцію до кращого поведінки (або, принаймні, дає кращі повідомлення про помилки). [...] Однак явний відносний імпорт є прийнятною альтернативою абсолютному імпорту, особливо коли йдеться про складні схеми пакету, де використання абсолютного імпорту було б зайвим багатослівним.