Относительно импорта в Python 3


Я хочу импортировать функцию из другого файла в том же каталоге.

иногда это работает для меня с from .mymodule import myfunction но иногда я получаю:

SystemError: Parent module '' not loaded, cannot perform relative import

иногда это работает с from mymodule import myfunction, но иногда я тоже получаю:

SystemError: Parent module '' not loaded, cannot perform relative import

Я не понимаю логики здесь, и я не мог найти никакого объяснения. Это выглядит совершенно случайным.

может кто-нибудь объяснить мне, в чем логика всего этого?

8 438

8 ответов:

к сожалению, этот модуль должен быть внутри упаковки, а также иногда его нужно запускать как скрипт. Любая идея, как я мог добиться этого?

это довольно часто, чтобы иметь такое расположение...

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()

...а 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

так, как вы должны его запустить...

python3 -m mypackage.myothermodule

...но это несколько многословно, и не очень хорошо сочетается с линией shebang, такой как #!/usr/bin/env python3.

самое простое исправление для этого случая, предполагая имя mymodule является уникальным в глобальном масштабе, можно было бы избежать, используя относительный импорт, и просто использовать...

from mymodule import as_int

...хотя, если это не уникально, или ваш структура пакета более сложная, вам нужно будет включить каталог, содержащий каталог вашего пакета в PYTHONPATH, и сделать это так...

from mypackage.mymodule import as_int

...или если вы хотите, чтобы он работал "из коробки", вы можете frob PYTHONPATH в коде сначала с этим...

import sys
import os

PACKAGE_PARENT = '..'
SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))

from mypackage.mymodule import as_int

это своего рода боль, но есть ключ к тому, почему в по электронной почте написано неким Гвидо ван Россумом...

я -1 на этом и на любом другом предложенном вертушки из __main__ механизм. Единственный вариант использования, похоже, запускает сценарии, которые происходят чтобы жить внутри каталога модуля, который я всегда видел как антимодель. Чтобы заставить меня передумать, тебе придется убедить меня в этом. это не так.

является ли запуск скриптов внутри пакета антипаттерном или нет, субъективно, но лично я нахожу его действительно полезным в пакете, который у меня есть, который содержит некоторые пользовательские виджеты wxPython, поэтому я могу запустить скрипт для любой из исходных файлов для отображения wx.Frame содержит только этот виджет для целей тестирования.

объяснение

с PEP 328

относительный импорт используйте атрибут __name_ _ модуля, чтобы определить, что позиция модуля в иерархии пакетов. Если имя модуля делает не содержит никакой информации о пакете (например, он установлен в ' _ _ main__') затем относительный импорт разрешается, как если бы модуль был верхнего уровня модуль, независимо от того, где на самом деле находится модуль в файле система.

в какой-то момент PEP 338 противоречит PEP 328:

... относительный импорт зависит от _ _ name__ для определения текущего позиция модуля в иерархии пакетов. В главном модуле значение _ _ name__ всегда ' _ _ main__', так откровенно, относительный импорт всегда будет терпеть неудачу (так как они работают только для модуля внутри пакет)

и чтобы решить эту проблему, PEP 366 введена переменная верхнего уровня __package__:

добавляя новый атрибут уровня модуля, этот PEP позволяет относительное импорт будет работать автоматически, если модуль выполняется с помощью - m переключатель. Небольшое количество шаблона в самом модуле позволит относительный импорт для работы, когда файл выполняется по имени. [...] Когда он [ атрибут] присутствует, относительный импорт будет основан на этом атрибуте а не модуль _ _ name__. [...] Если основной модуль задан его именем файла, то __пакет__ атрибут будет установлен в нет. [...]когда система импорта сталкивается с явным относительным импортом в a модуль без __package__ set (или с установленным значением 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

все файлы в пакета начните с тех же 2 строк кода:

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: установите _ _ пакет _ _ вручную

пожалуйста, рассматривайте его как доказательство концепции, а не фактическое решение. Он не очень хорошо подходит для использования в реальном коде.

PEP 366 имеет обходной путь к этой проблеме, однако, это неполный, потому что установка __package__ одного недостаточно. Вам нужно будет импортировать по крайней мере N предыдущие пакеты в иерархии модулей, где N - количество родительских каталогов (относительно каталога скрипта), в которых будет выполняться поиск импортируемого модуля.

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

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

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

  3. импорт родительского модуля текущего модуля с помощью его полное имя

  4. Set __package__ к полному имени от 2

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

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

давайте проверим, что теперь можно запустить standalone.py как a сценарий:

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 рекомендует использовать абсолютный импорт, но заявляет, что в некоторых сценариях явный относительный импорт допустим:

абсолютный импорт рекомендуется, так как они обычно более читабельны и, как правило, лучше себя ведут (или хотя бы дают лучшая ошибка сообщения.) [...] Однако явный относительный импорт является приемлемым альтернатива абсолютному импорту, особенно при работе со сложными макеты пакетов, где использование абсолютного импорта было бы излишне многословный.

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

положите это в ваш пакет __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.

если оба пакета находятся в пути импорта (sys.путь), и модуль / класс, который вы хотите находится в example/example.py, затем для доступа к классу без относительного импорта попробуйте:

from example.example import fkt

чтобы обойти эту проблему, я придумал решение с упаковка, который работал для меня в течение некоторого времени. Он добавляет верхний каталог к пути lib:

import repackage
repackage.up()
from mypackage.mymodule import myfunction

переупаковка может сделать относительный импорт, который работает в широком диапазоне случаев, используя интеллектуальную стратегию (проверка стека вызовов).

надеюсь, это будет полезно для кого - то там-я прошел через полдюжины сообщений stackoverflow, пытаясь выяснить относительный импорт, подобный тому, что было опубликовано выше здесь. Я настроил все, как предлагалось, но я все еще бил ModuleNotFoundError: No module named 'my_module_name'

так как я просто развивался локально и играл вокруг, я не создал / запустить . Я также, по-видимому, не установил свой PYTHONPATH.

я понял, что когда я запустил свой код, как я был, когда тесты были в том же каталоге, что и модуль, я не смог найти свой модуль:

$ python3 test/my_module/module_test.py                                                                                                               2.4.0
Traceback (most recent call last):
  File "test/my_module/module_test.py", line 6, in <module>
    from my_module.module import *
ModuleNotFoundError: No module named 'my_module'

однако, когда я явно указал путь вещи начали работать:

$ PYTHONPATH=. python3 test/my_module/module_test.py                                                                                                  2.4.0
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

Итак, в случае, если кто-то попробовал несколько предложений, считает, что их код структурирован правильно и все еще находится в аналогичной ситуации, как и я, попробуйте любое из следующих действий, если вы не экспортируете текущий каталог в свой PYTHONPATH:

  1. Запустите свой код и явно включите путь следующим образом: $ PYTHONPATH=. python3 test/my_module/module_test.py
  2. чтобы не звонить PYTHONPATH=. создать setup.py файл с содержимым, как показано ниже, и запустить python setup.py development чтобы добавить пакеты в путь:
# setup.py
from setuptools import setup, find_packages

setup(
    name='sample',
    packages=find_packages()
)

для меня мне нужно было запустить python3 из основного каталога, чтобы заставить его работать. Например, если проект имеет следующую структуру:


project_demo

| - main.py

| - - some_package

| - - - - __ init __ .ру

| - - - - project_configs.py

| - - тест

| - - - - test_project_configs.py


устранение: Я бы запустил python3 внутри project_demo а то из some_package import project_configs.