Как я могу использовать относительный импорт в Python3 с блоком if __name__='__main__'?

Я делаю пакет, и модули в этом пакете имеют код внутри блоков if __name__=='__main__': для целей тестирования. Но мои попытки использовать относительный импорт в этих модулях вызывают ошибки.

Я прочитал эту тему и миллиард других: Относительный импорт в миллиардный раз

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


Это мой пример проекта Python:

mypackage
- module1.py
- module2.py
- __init__.py

__init__.py и module2.py пусты

module1.py содержит:

import module2

# module1 contents

if __name__=="__main__":
    # Some test cases for the contents of this module
    pass

Это отлично работает в Python2. Я могу импортировать модуль1 из других проектов в любое место на своем компьютере, а также могу запускать модуль1 напрямую и запускать код в блоке if.

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

>>> from mypackage import module1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\_MyFiles\Programming\Python Modules\mypackage\module1.py", line 1, in <module>
    import module2
ModuleNotFoundError: No module named 'module2'

Поэтому я попытался изменить первую строку на from . import module2, и это исправило ее, чтобы я мог успешно импортировать модуль из любого места. Но затем, когда я пытаюсь запустить module1 напрямую как скрипт, я получаю эту ошибку:

Traceback (most recent call last):
  File "C:/_MyFiles/Programming/Python Modules/mypackage/module1.py", line 1, in <module>
    from . import module2
ImportError: cannot import name 'module2' from '__main__' (C:/_MyFiles/Programming/Python Projects/pgui/mypackage/module1.py)

Я не хочу открывать консоль и набирать python -m myfile каждый раз, когда работаю над модулем и хочу запускать его непосредственно как скрипт.

Я хочу иметь возможность работать с модулями, не добавляя их родительскую папку в PYTHONPATH, используя относительный импорт, как в Python2.

Есть ли лучший обходной путь или решение этих проблем?


person pyjamas    schedule 02.02.2019    source источник
comment
Я не хочу открывать консоль и набирать python -m myfile каждый раз, когда работаю над модулем и хочу запускать его непосредственно как скрипт. - в отличие от того, что, открывая консоль и набирая python myfile.py? Как вы работаете с вашими файлами в настоящее время? (Также это python -m packagename.modulename для подмодулей пакета, а не python -m modulename.)   -  person user2357112 supports Monica    schedule 02.02.2019
comment
Я предполагаю, что другие специалисты расскажут вам, как правильно использовать Python, однако вы можете просто использовать `try ..., кроме ImportError`, чтобы импортировать его так, как это работает. В Dive Into Python есть пример.   -  person glumplum    schedule 02.02.2019
comment
@user2357112 user2357112 В настоящее время я запускаю свои файлы через PyCharm IDE, нажимая кнопку запуска в графическом интерфейсе. Я понимаю, что могу добавить -m в конфигурацию, но это кажется хлопотным, и я бы предпочел, чтобы мой код работал при нормальном запуске, как в Python2. И я хотел бы отправить это кому-нибудь для использования без необходимости предупреждать их и объяснять, как это запустить, чтобы избежать ошибок относительного импорта сбоя с блоками main с загадочной ошибкой.   -  person pyjamas    schedule 02.02.2019
comment
@glumplum Спасибо, это похоже на жизнеспособный обходной путь, если нет «правильного» способа.   -  person pyjamas    schedule 02.02.2019
comment
Отвечает ли это на ваш вопрос? Python3 правильный способ импорта относительного или абсолютного?   -  person Ani Menon    schedule 03.07.2020
comment
@AniMenon Нет, я принял ответ Anoop, поскольку никто больше не дал, похоже, что ответ на вопрос «Как я могу использовать относительный импорт в Python3 с if __name__='__main__' block?» просто «Вы не можете   -  person pyjamas    schedule 03.07.2020
comment
@Esostack Правильно, когда вы запускаете скрипт, вы не можете получить доступ к чему-либо из его родительского каталога относительно. Но если это часть пакета и вы запускаете его как модуль, вы можете относительно импортировать его внутри пакета.   -  person Ani Menon    schedule 03.07.2020
comment
Если вы хотите добавить проверку файлов с проверкой на наличие main, а также хотите импортировать файл в другие модули, используйте ту же проверку на main на уровне импорта: if name == ' main': из модуля импорта mypackage1 иначе: из модуля импорта .mypackage1   -  person strawbot    schedule 24.12.2020


Ответы (3)


Согласно документации по модулям, для __main__ модулей вы должны использовать абсолютный импорт.

Обратите внимание, что относительный импорт основан на имени текущего модуля. Поскольку имя основного модуля всегда «main», модули, предназначенные для использования в качестве основного модуля приложения Python, всегда должны использовать абсолютный импорт.

Так что просто измените строку импорта в module1.py на:

from mypackage import module2

Все остальное остается прежним.

person Anoop R Desai    schedule 02.02.2019
comment
Есть ли ошибка в вашем коде? module2.py, так как первая строка __init__.py дает неразрешенную ошибку ссылки. И удаляя это, я все еще получаю ту же ошибку, что и раньше в Python3. - person pyjamas; 02.02.2019
comment
О да, похоже, у меня сейчас такая же проблема. Я нашел решение; обновит мой ответ. Извини за это. - person Anoop R Desai; 02.02.2019
comment
Спасибо, но для этого по-прежнему требуется добавить родительскую папку пакета в PYTHONPATH, иначе произойдет сбой. Это означает, что когда я впервые пишу код, я должен использовать относительный импорт, а затем я реорганизую все свои операторы импорта, чтобы они были абсолютными, как только я решу сделать его модулем? Это также означает, что мой код не работает для всех, кто загружает мой пакет в виде zip-архива, если они сначала не добавят его в свой собственный PYTHONPATH. Таким образом, хотя простое отсутствие использования относительного импорта приводит к тому, что код работает, это не ответ на мой ОП о том, как заставить работать относительный импорт, как в Python2. - person pyjamas; 02.02.2019
comment
Добавляется ли проверка __name__ == "__main__" только для тестирования? В идеале, на мой взгляд, у вас должен быть только один модуль main, который станет отправной точкой для вашего кода. Для тестирования можно использовать другой подход. Таким образом, вы можете использовать относительный импорт везде, кроме модуля main. Кроме того, если вы планируете распространять его в виде zip-архива, вам не нужны несколько точек входа для кода (если только для этого нет особой причины). - person Anoop R Desai; 02.02.2019
comment
Да это только для пробы. Это проект PyQt5 с одним модулем для основного графического интерфейса и другими модулями для различных других виджетов, диалогов и функций, которые используются в основном графическом интерфейсе. Таким образом, для каждого модуля у меня есть блок __name__ == "__main__", который создает тестовые входные данные и создает только виджет, поэтому мне не нужно открывать основной графический интерфейс и открывать его вручную. Какова правильная альтернатива? Должен ли я просто иметь отдельный тестовый файл для каждого, например mydialog_test.py, который использует относительный импорт и имеет тестовый код не в блоке main? - person pyjamas; 03.02.2019
comment
Это интересно. Я не использовал PyQt5, но у меня было аналогичное требование для одного из моих проектов с использованием Tkinter. У нас был точно такой же сценарий, когда мы использовали основную проверку для быстрого тестирования. Я только что откопал этот проект — похоже, мы разделили его на несколько пакетов и использовали абсолютный импорт для импорта определенных классов из разных модулей — что-то вроде from package.module import class. Теперь я не уверен, есть ли лучший способ. - person Anoop R Desai; 04.02.2019

Пакет Python — это не просто папка, в которую вы помещаете свой код, и поведение при импорте зависит не только от того, в какую папку вы поместили свой код.

Когда вы запускаете свой файл напрямую, вы не запускаете его как часть пакета. Инициализация на уровне пакета не выполняется, и Python даже не распознает существование пакета. В Python 2 наличие неявного относительного импорта означало, что голый import module2 разрешался либо в абсолютный, либо в неявный относительный импорт, скрывая проблему, но структура импорта по-прежнему нарушена. В Python 3 неявный относительный импорт пропал (по уважительной причине), поэтому проблема видна сразу.

Запуск подмодуля пакета напрямую по имени файла не очень хорошо работает. В наши дни я считаю, что стандартом является либо использование -m, либо использование сценария точки входа верхнего уровня, который вызывает функциональность подмодуля.

В любом случае, есть способ заставить работать по имени файла, но это много шаблонов. Разработчики PEP 366, по-видимому, планировали __package__ = 'appropriate.value' назначение, чтобы сделать относительный импорт работает правильно, но на самом деле этого недостаточно, даже если вы исправите путь импорта. Вы также должны инициализировать родительский пакет вручную, иначе вы получите «SystemError: родительский модуль« foo »не загружен, не может выполнить относительный импорт», как только вы попытаетесь запустить относительный импорт. Полный шаблон больше похож на

import os.path
import sys
if __name__ == '__main__' and __package__ is None:
    __package__ = 'mypackage'
    right_import_root = os.path.abspath(__file__)
    for i in range(__package__.count('.') + 2):
        right_import_root = os.path.dirname(right_import_root)

    # sys.path[0] is usually the right sys.path entry to replace, but this
    # may need further refinement in the presence of anything else that messes
    # with sys.path
    sys.path[0] = right_import_root
    __import__(__package__)

Это происходит после таких вещей, как будущий импорт, но перед любым импортом, который зависит от вашего пакета.

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

person user2357112 supports Monica    schedule 02.02.2019

Я оказался в похожем сценарии, и это меня очень беспокоило, пока я не понял, как должен работать импорт модулей и пакетов.

Рассмотрим следующую структуру

mydir
- project
  - __init__.py
  - module1.py
  - module2.py

Содержимое module1 и module2 выглядит следующим образом

module1.py

print("moudule1")

module2.py

от . модуль импорта1

print("Module 2")

if __name__ == '__main__':
    print("Executed as script")

Теперь, если я открою repl вне каталога пакета и попытаюсь сделать импорт, он работает

Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from package import module2
Module 1
Module 2
>>> sys.path
['', '/usr/lib/python36.zip', '/usr/lib/python3.6', '/usr/lib/python3.6/lib-dynload', '/home/rbhanot/.local/lib/python3.6/site-packages', '/usr/local/lib/python3.6/dist-packages', '/usr/lib/python3/dist-packages']

Обратите внимание на sys.path, как вы можете видеть, он содержит текущий каталог, в котором я нахожусь, в качестве первого элемента, что означает, что все мои импорты будут сначала искать в моем текущем каталоге.

Теперь, если я зайду в каталог пакетов, а затем открою repl и попытаюсь сделать тот же импорт, посмотрите, что произойдет.

Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from . import module2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'module2'
>>> import module2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/rbhanot/python-dotfiles/python3/modules-packages/mydir/package/module2.py", line 1, in <module>
    from . import module1
ImportError: attempted relative import with no known parent package
>>> import module1
Module 1
>>>

Как вы можете видеть, импорт терпит неудачу, причина сбоя в том, что когда я пытаюсь импортировать модуль из пакета, Python ищет в sys.path любой пакет с именем package, поскольку я не смог найти ни одного, поэтому импорт не удается. Но импорт модуля1 работает, потому что он находится в текущем каталоге.

Вне пакета я могу выполнить скрипт как

python3 -m package.module2                                                                              2 ↵
Module 1
Module 2
Executed as script

Хотя я могу выполнить скрипт, но это не то, как его предполагается использовать. Помните, что пакеты — это библиотека кода, которую необходимо использовать совместно, и в ней не должно быть кода, который может быть непосредственно запущен через командную строку. Пакеты и модули внутри пакетов предназначены только для импорта, а затем после импорта вы можете написать свои сценарии, которые выполняются через командную строку, поместив в нее предложение __name__.

person Rohit    schedule 02.02.2019