Вопрос-Ответ

Relative imports in Python 3

Относительный импорт в 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 - количество родительских каталогов (относительно каталога скрипта), в которых будет выполняться поиск импортируемого модуля.

Таким образом,


  1. Добавьте родительский каталог N-го предшественника текущего модуля в sys.path


  2. Удалите каталог текущего файла из sys.path


  3. Импортируйте родительский модуль текущего модуля, используя его полное имя


  4. Установите __package__ полное имя из 2


  5. Выполните относительный импорт


Я позаимствую файлы из решения № 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

Следующие шаги -


  1. Замените явный относительный импорт эквивалентным абсолютным импортом


  2. Установите 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:


  1. Добавьте родительский каталог пакета в 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

  2. Замените относительный импорт абсолютным импортом:


    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()
python python-3.x