Відносний імпорт у Python 3


712

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

Іноді це працює для мене, 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
Це означає, що ви запускаєте модуль всередині пакета як сценарій. Запускайте сценарії тільки поза пакетом.
Martijn Pieters

3
Ймовірно, вам слід визначити умови, які ви маєте в «тих, хто іноді» ви згадуєте. Я розумію, ви не означає, що у вас випадкові помилки.
Жоакін

15
@MartijnPieters: ну, на жаль, цей модуль повинен знаходитися всередині пакету, і його також потрібно іноді виконувати як сценарій. Будь-яка ідея, як я міг цього досягти?
Джон Сміт за бажанням

22
@JohnSmithO optional: Змішування сценаріїв всередині пакетів є складним і його слід уникати, якщо це можливо. Використовуйте скрипт для обгортки, який імпортує пакет і замість цього виконує вашу функцію 'scriptpty'.
Martijn Pieters

3
Здається, прикро. Я створив основний модуль з класами / методами, які можуть аналізувати / аналізувати певний тип файлів, а також маю (головним чином для себе) окремі вторинні модулі та сценарії, які імпортують їх - вони можуть масажувати / перетворювати ці файли. Але мені також подобається мати можливість передавати цей єдиний ядерний файл (не весь складний пакет) кінцевому користувачеві, щоб він міг легко розмістити його поруч зі своїм файлом та запустити його. У цьому "режимі скриптів" він аналізує і аналізує файл та кодування, підбирає різні поля / значення / спеціальні символи та дає звіт. Але він фактично не змінює файл. Антидіаграма?
Джон Кумбс

Відповіді:


526

на жаль, цей модуль повинен знаходитись всередині пакету, а також його іноді потрібно виконувати як сценарій. Будь-яка ідея, як я міг цього досягти?

Досить поширеним є такий макет ...

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

... або якщо ви хочете, щоб це спрацьовувало "поза коробкою", ви можете спершу розморотити 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 ) )
Alex-Bogdanov

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

9
Гвідо знову вражає: нанизуючи речі, які могли бути корисними. Ну, цього більше не буде.
javadba

4
Це найсумніша річ, яку я коли-небудь бачив про Python.
AtilioA

262

Пояснення

Від PEP 328

Відносний імпорт використовує атрибут __name__ модуля для визначення положення цього модуля в ієрархії пакетів. Якщо ім'я модуля не містить жодної інформації про пакет (наприклад, для нього встановлено значення __main__), то відносний імпорт вирішується так, як якщо б модуль був модулем верхнього рівня , незалежно від того, де цей модуль фактично розташований у файловій системі.

У якийсь момент PEP 338 конфліктував з PEP 328 :

... відносний імпорт розраховує на __name__ для визначення поточного положення модуля в ієрархії пакетів. В основному модулі, значення __name__ завжди «__main__» , тому явний відносний імпорт завжди буде терпіти невдачі (як вони тільки для роботи модуля всередині пакету)

і щоб вирішити цю проблему, PEP 366 представив змінну верхнього рівня __package__:

Додаючи новий атрибут рівня модуля, цей PEP дозволяє відносному імпорту працювати автоматично, якщо модуль виконується за допомогою перемикача -m . Невелика кількість котла в самому модулі дозволить відносному імпорту працювати, коли файл виконується по імені. [...] Якщо він [атрибут] присутній, відносний імпорт буде базуватися на цьому атрибуті, а не на атрибуті модуля __name__ . [...] Коли головний модуль буде вказаний своїм ім'ям файла, то для атрибута __package__ буде встановлено значення None . [...] Коли система імпорту стикається з явним відносним імпортом у модулі без набору __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встановіть пакунок у каталозі пакунків вашого сайту ) (крок 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, я отримую помилку: `Файл" /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, якщо "nltk "не в p] Файл" /usr/local/lib/python3.5/dist-packages/nltk/decorators.py ", рядок 23, в <listcomp> sys.path = [p для p в sys.path, якщо" nltk "not in p] TypeError: аргумент типу" PosixPath "не ітерабельний"
користувач

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

84

Помістіть це все у __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
Я також думаю, що це заслуговує на більше голосів. Якщо ввести це в кожний __init__.py, в основному вирішите всі відносні помилки імпорту.
frankliuao

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

@pianoJames Я знаю, що ти маєш на увазі, це (мабуть, після багато накручування) магічне виправлення здається трохи надто простим. Але це працює. Було б цікаво не знати з тих, що знають, якщо це має негативні побічні ефекти.
Jon

Я зараз цим користуюся: і поки що добре.
javadba

37

Я зіткнувся з цим питанням. Вирішення способу взлома імпортується через блок 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)
Avi

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

@Perkins Ну ... в більшості випадків це не було б . Я думаю, що відносний імпорт може бути винятком.
wizzwizz4

8

Сподіваємось, це буде корисно для когось там - я пройшов півдюжини публікацій stackoverflow, намагаючись з'ясувати відносний імпорт, подібний до того, що розміщено тут вище. Я все налаштував так, як було запропоновано, але все одно вдарив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 /, а потім виконаю a

from some_package import project_configs

4

Щоб усунути цю проблему, я розробив рішення з пакетом перепакування , який працював на мене певний час. Він додає верхній каталог до шляху 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.