Относительный импорт в Python 3
Я хочу импортировать функцию из другого файла в том же каталоге.
Обычно работает одно из следующих действий:
from .mymodule import myfunction
from mymodule import myfunction
...но другой выдает мне одну из этих ошибок:
ImportError: attempted relative import with no known parent package
ModuleNotFoundError: No module named 'mymodule'
SystemError: Parent module '' not loaded, cannot perform relative import
Почему это?
Переведено автоматически
Ответ 1
к сожалению, этот модуль должен быть внутри пакета, и иногда его также нужно запускать как скрипт. Есть идеи, как я мог бы этого добиться?
Довольно часто подобный макет используется...
main.py
mypackage/
__init__.py
mymodule.py
myothermodule.py
...с помощью mymodule.py
вот так...
#!/usr/bin/env python3
# Exported function
def as_int(a):
return int(a)
# Test function for module
def _test():
assert as_int('1') == 1
if __name__ == '__main__':
_test()
...a myothermodule.py
вот так...
#!/usr/bin/env python3
from .mymodule import as_int
# Exported function
def add(a, b):
return as_int(a) + as_int(b)
# Test function for module
def _test():
assert add('1', '1') == 2
if __name__ == '__main__':
_test()
...и main.py
вот так...
#!/usr/bin/env python3
from mypackage.myothermodule import add
def main():
print(add('1', '1'))
if __name__ == '__main__':
main()
...который отлично работает при запуске main.py
или mypackage/mymodule.py
, но не работает с mypackage/myothermodule.py
из-за относительного импорта...
from .mymodule import as_int
Предполагается, что вы должны запустить его, используя опцию -m и указав путь в системе модулей Python (а не в файловой системе)...
python3 -m mypackage.myothermodule
...но это несколько многословно и плохо сочетается с шаблонной строкой типа #!/usr/bin/env python3
.
Альтернативой является отказ от использования относительного импорта и просто использование...
from mypackage.mymodule import as_int
В любом случае вам нужно будет запустить родительский каталог mypackage
или добавить этот каталог в PYTHONPATH
(любой из них гарантирует, что он mypackage
находится в пути поиска модуля sys.path ). Или, если вы хотите, чтобы это работало "из коробки", вы можете сначала встроить PYTHONPATH
в код с помощью этого...
import sys
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))
from mypackage.mymodule import as_int
Это своего рода проблема, но в электронном письме, написанном неким Гвидо ван Россумом, есть подсказка, почему...
Я -1 за это и за любые другие предлагаемые изменения в
__main__
оборудовании. Похоже, единственным вариантом использования является запуск скриптов, которые находятся внутри каталога модуля, который я всегда рассматривал как антипаттерн. Чтобы заставить меня изменить свое мнение, вам придется убедить меня, что это не так.
Является ли запуск скриптов внутри пакета антишаблоном или нет, субъективно, но лично я нахожу это действительно полезным в имеющемся у меня пакете, который содержит несколько пользовательских виджетов wxPython, поэтому я могу запустить скрипт для любого из исходных файлов, чтобы отобразить wx.Frame
содержащий только этот виджет для целей тестирования.
Ответ 2
Объяснение
Из PEP 328
Относительный импорт использует атрибут __name__ модуля для определения положения этого модуля в иерархии пакетов. Если имя модуля не содержит никакой информации о пакете (например, оно имеет значение '__main__'), то относительный импорт разрешается так, как если бы модуль был модулем верхнего уровня, независимо от того, где модуль фактически расположен в файловой системе.
В какой-то момент PEP 338 конфликтовал с PEP 328:
... относительный импорт зависит от __name__ для определения позиции текущего модуля в иерархии пакетов. В основном модуле значение __name__ всегда '__main__', поэтому явный относительный импорт всегда завершается ошибкой (поскольку он работает только для модуля внутри пакета)
и для решения проблемы в PEP 366 была введена переменная верхнего уровня __package__
:
Благодаря добавлению нового атрибута уровня модуля, этот PEP позволяет относительному импорту работать автоматически, если модуль выполняется с использованием переключателя -m. Небольшое количество шаблонов в самом модуле позволит относительному импорту работать, когда файл выполняется по имени. [...] Когда он [атрибут] присутствует, относительный импорт будет основан на этом атрибуте, а не на атрибуте модуля __name__. [...] Когда основной модуль указан по его имени файла, то для __package__ атрибута будет установлено значение None. [...] Когда система импорта обнаруживает явный относительный импорт в модуле без __package__ set (или с установленным значением None), она вычислит и сохранит правильное значение (__name__.rpartition('.')[0] для обычных модулей и __name__ для модулей инициализации пакета)
(выделено мной)
Если __name__
является '__main__'
, __name__.rpartition('.')[0]
возвращает пустую строку. Вот почему в описании ошибки есть пустой строковый литерал:
SystemError: Parent module '' not loaded, cannot perform relative import
Соответствующая часть PyImport_ImportModuleLevelObject
функции CPython:
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
Все файлы в пакете начинаются с одних и тех же 2 строк кода:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
Я включаю эти две строки только для того, чтобы сделать порядок операций очевидным. Мы можем полностью игнорировать их, поскольку они не влияют на выполнение.
__init__.py и module.py содержат только эти две строки (т.Е. Фактически они пустые).
автономный.py дополнительно пытается импортировать модуль.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
На этот раз автономно.py импортирует модуль.py из пакета package, используя следующий относительный импорт
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
Это позволяет нам выполнять автономно.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: используйте абсолютный импорт и setuptools
Следующие шаги -
Замените явный относительный импорт эквивалентным абсолютным импортом
Установите
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
Давайте убедимся, что теперь его можно запускать автономно.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 рекомендует использовать абсолютный импорт, но указывает, что в некоторых сценариях допустим явный относительный импорт:
Рекомендуется использовать абсолютный импорт, поскольку он обычно более удобочитаем и, как правило, лучше работает (или, по крайней мере, выдает лучшие сообщения об ошибках). [...] Однако явный относительный импорт является приемлемой альтернативой абсолютному импорту, особенно при работе со сложными макетами пакетов, где использование абсолютного импорта было бы излишне подробным.
Ответ 3
Поместите это в __init__.py файл вашего пакета:
# For relative imports to work in Python 3.6
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
Предполагая, что ваш пакет выглядит следующим образом:
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module1.py
│ │ └── module2.py
│ └── setup.py
Теперь используйте обычный импорт в вашем пакете, например:
# in module2.py
from module1 import class1
Это работает как в python 2, так и в 3.
Ответ 4
Я столкнулся с этой проблемой. Обходным путем является импорт через блок if / else следующим образом:
#!/usr/bin/env python3
#myothermodule
if __name__ == '__main__':
from mymodule import as_int
else:
from .mymodule import as_int
# Exported function
def add(a, b):
return as_int(a) + as_int(b)
# Test function for module
def _test():
assert add('1', '1') == 2
if __name__ == '__main__':
_test()