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


715

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

Иногда это работает для меня, 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

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

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


76
Это означает, что вы запускаете модуль внутри пакета как скрипт. Только запускать скрипты из вне пакета.
Мартин Питерс

3
Вероятно, вы должны определить условия, которые у вас есть «иногда», которые вы упоминаете. Я так понимаю, вы не имеете в виду случайные ошибки.
Хоакин

15
@MartijnPieters: ну, к сожалению, этот модуль должен быть внутри пакета, и иногда его также нужно запускать как скрипт. Есть идеи, как мне этого добиться?
Джон Смит Необязательно

22
@JohnSmithOptional: Смешивать скрипты внутри пакетов сложно, и их следует избегать, если это вообще возможно. Используйте скрипт-обертку, который импортирует пакет и вместо этого запускает вашу функцию «scripty».
Мартин Питерс

3
Кажется неудачным. Я сделал основной модуль с классами / методами, которые могут анализировать / анализировать определенный тип файлов, и у меня также есть (в основном для себя) отдельные вторичные модули и скрипты, которые его импортируют - они могут преобразовывать / преобразовывать эти файлы. Но я также хотел бы иметь возможность передать этот основной файл (не целый комплексный пакет) конечному пользователю, чтобы он мог легко разместить его рядом со своим файлом и запустить его. В этом «режиме сценария» он анализирует и анализирует файл и кодировку, подсчитывает различные поля / значения / специальные символы и выдает отчет. Но это на самом деле не изменяет файл. Анти-шаблон?
Джон Кумбс

Ответы:


529

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

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

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

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

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

from mymodule import as_int

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

from mypackage.mymodule import as_int

... или если вы хотите, чтобы он работал "из коробки", вы можете сначала добавить 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только содержащий этот виджет для тестирования.


7
Лучший способ получить SCRIPTDIR приведен в комментарии Импортировать модуль из относительного пути, как os.path.realpath(os.path.dirname(inspect.getfile(inspect.currentframe())))если бы вы были уверены, что ваш модуль всегда имеет свойство, которое fileвы также можете использовать os.path.realpath(os.path.dirname(__file__)).
marcz

2
Вы можете расширить свой PYTHONPATH, применив более короткий и читаемый фрагмент кода: sys.path.append( os.path.join( os.path.dirname(__file__), os.path.pardir ) )
Алекс-Богданов

12
...which I've always seen as an antipattern.Я не понимаю, как это анти-паттерн ... Кажется, было бы очень удобно просто сделать относительный импорт интуитивно понятным. Я просто хочу иметь возможность импортировать вещи, которые я знаю, находятся в одном каталоге. Интересно, каково было его рассуждение
YungGun

9
Гвидо снова наносит удар: отнимает вещи, которые могли бы быть полезными. Ну, этого больше не будет.
Джавадба

4
Это самая печальная вещь, которую я когда-либо видел в Python.
AtilioA

265

объяснение

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

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

  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

На этот раз 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: Используйте абсолютный импорт и установочные инструменты

Шаги -

  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устанавливает пакет в ваш каталог 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 :

  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
Можно ли установить __package__вручную, если имя __main__для решения проблемы?
Пауло Скардин

Спасибо, хорошие ответы! Я смог загрузить модуль с помощью impмодуля и установить его __package__соответствующим образом, но результат явно является анти-паттерном.
Пауло Скардин

Я получаю ошибку AttributeError: 'PosixPath' object has no attribute 'path'.
пользователь

Спасибо за быстрый ответ. Я использую пакет nltk, получаю сообщение об ошибке: `File" /usr/local/lib/python3.5/dist-packages/nltk/__init__.py ", строка 115, в <module> от nltk.decorators декоратор импорта, запомните файл "/usr/local/lib/python3.5/dist-packages/nltk/decorators.py", строка 23, в <module> sys.path = [p для p в sys.path if "nltk "Файл не в p]" /usr/local/lib/python3.5/dist-packages/nltk/decorators.py ", строка 23, в <listcomp> sys.path = [p для p в sys.path if" nltk "not in p] Ошибка типа: аргумент типа 'PosixPath' не повторяется '
пользователь

1
Вы также можете импортировать файл по пути к файлу (тоже относительно): docs.python.org/3/library/…
Ctrl-C

87

Поместите это в файл вашего пакета __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.


1
делает это, если мы упаковываем это как weel
Алекс Пуннен

1
Я также думаю, что это заслуживает большего количества голосов. Включение этого в каждый __init__.pyбудет в основном разрешать все относительные ошибки импорта.
frankliuao

3
Я не могу говорить за других, но я склонен избегать модификаций, sys.pathпотому что я обеспокоен тем, что это может повлиять на другой код. (Частично это потому, что я не знаю тонкостей того, как это работает.)
pianoJames

@pianoJames Я знаю, что вы имеете в виду, это (казалось бы, после долгих разговоров) волшебное исправление кажется слишком легким. Но это работает. Было бы интересно не знать от тех, кто знает, если это имеет негативные побочные эффекты.
Джон

Я использую это сейчас: и пока все хорошо.
Джавадба

38

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

29
это не очень хорошее решение. Кроме того, голые except:это плохо. используйте except ImportError:вместо этого!
ThiefMaster

6
Это SystemErrorздесь. (Py 3.4)
Ави

8
Это не страшная идея, но было бы лучше определить, какой импорт использовать, а не пытаться / исключать. Нечто подобное if __name__ == '__main__': from mymod import as_int; else: from .mymod import as_int.
Перкинс

@ Перкинс Ну ... в большинстве случаев это не так . Я думаю, что относительный импорт может быть исключением.
wizzwizz4

8

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

Так как я только разрабатывал локально и играл, я не создал / не запустил setup.pyфайл. Я также, очевидно, не установил свой 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()
)

6

Мне нужно было запустить python3 из основного каталога проекта, чтобы он работал.

Например, если проект имеет следующую структуру:

project_demo/
├── main.py
├── some_package/
   ├── __init__.py
   └── project_configs.py
└── test/
    └── test_project_configs.py

Решение

Я бы запустил python3 внутри папки project_demo /, а затем выполнил бы

from some_package import project_configs

4

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

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

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


Безусловно самое простое решение! Спасибо!
CodingInCircles

1
Спасибо! Вместо того, чтобы пытаться дать лучший ответ, я попытался дать полезный ответ :-)
fralau

3

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

from example.example import fkt

1

Я думаю, что лучшее решение - создать пакет для вашего модуля: вот больше информации о том, как это сделать.

Если у вас есть пакет, вам не нужно беспокоиться об относительном импорте, вы можете просто выполнить абсолютный импорт.


0

У меня была похожая проблема: мне нужен был сервис Linux и плагин cgi, которые используют общие константы для взаимодействия. «Естественный» способ сделать это - поместить их в init .py пакета, но я не могу запустить плагин cgi с параметром -m.

Мое окончательное решение было похоже на Решение № 2 выше:

import sys
import pathlib as p
import importlib

pp = p.Path(sys.argv[0])
pack = pp.resolve().parent

pkg = importlib.import_module('__init__', package=str(pack))

Недостатком является то, что вы должны ставить перед константами (или общими функциями) префикс pkg:

print(pkg.Glob)
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.