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

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

vaultah
источник
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.

AM5
источник
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()
)
LaCroixed
источник
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

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

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

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

from example.example import fkt
Salt999
источник
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)
Томас Хекманн
источник