После ошибки пакета верхнего уровня в относительном импорте
Кажется, здесь уже есть несколько вопросов об относительном импорте в Python 3, но, пройдя многие из них, я так и не нашел ответа на свой вопрос. так вот в чем вопрос.
У меня есть пакет, показанный ниже
package/
__init__.py
A/
__init__.py
foo.py
test_A/
__init__.py
test.py
и у меня есть одна строка в test.py:
from ..A import foo
сейчас я в папке package
и я бегу
python -m test_A.test
Я получил сообщение
"ValueError: attempted relative import beyond top-level package"
но если я в родительской папке package
Например, я бегу:
cd ..
python -m package.test_A.test
Все отлично.
Теперь мой вопрос: когда я нахожусь в папке package
и я запускаю модуль внутри подпакета test_A как test_A.test
исходя из моего понимания, ..A
поднимается только на один уровень, который все еще находится в пределах package
папка, почему выдает сообщение beyond top-level package
, В чем именно причина этого сообщения об ошибке?
16 ответов
Эта же проблема отмечена в этом вопросе с более последовательным ответом: импорт пакета родного брата
Почему это не работает? Это потому, что python не записывает, откуда был загружен пакет. Итак, когда вы делаете python -m test_A.test
, это в основном просто отбрасывает знание о том, что test_A.test
на самом деле хранится в package
(т.е. package
не считается пакетом). Попытка from ..A import foo
пытается получить доступ к информации, которой у него больше нет (например, каталоги соседних узлов загруженного местоположения). Это концептуально похоже на разрешение from ..os import path
в файле в math
, Это было бы плохо, потому что вы хотите, чтобы пакеты были разными. Если им нужно использовать что-то из другого пакета, им следует обращаться к ним глобально с помощью from os import path
и пусть Python решит, где это с $PATH
а также $PYTHONPATH
,
Когда вы используете python -m package.test_A.test
затем с помощью from ..A import foo
решает просто отлично, потому что он отслеживал, что в package
и вы просто получаете доступ к дочернему каталогу загруженного местоположения.
Почему Python не считает текущий рабочий каталог пакетом? НЕТ КЛИВА, но черт возьми, это было бы полезно.
import sys
sys.path.append("..") # Adds higher directory to python modules path.
Попробуй это. Работал на меня.
Допущение:
Если вы находитесь в package
каталог, A
а также test_A
это отдельные пакеты.
Заключение:..A
импорт разрешен только в пакете.
Дальнейшие заметки:
Делать относительный импорт доступным только в пакетах полезно, если вы хотите, чтобы пакеты могли быть размещены на любом пути, расположенном на sys.path
,
Ни одно из этих решений не работало для меня в 3.6 с такой структурой папок, как:
package1/
subpackage1/
module1.py
package2/
subpackage2/
module2.py
Моей целью было импортировать из module1 в module2. Что, наконец, сработало для меня, было, как ни странно
import sys
sys.path.append(".")
Обратите внимание на одну точку, в отличие от упомянутых выше двухточечных решений.
Изменить: следующее помогло прояснить это для меня:
import os
print (os.getcwd())
В моем случае рабочий каталог был (неожиданно) корневым каталогом проекта.
В Python это очень сложно.
Сначала я прокомментирую, почему у вас возникла эта проблема, а затем упомяну два возможных решения.
- Что происходит?
Вы должны принять во внимание этот абзац из документации Python:
Обратите внимание, что относительный импорт основан на имени текущего модуля. Поскольку имя главного модуля всегда " main ", модули, предназначенные для использования в качестве основного модуля приложения Python, всегда должны использовать абсолютный импорт.
А также следующее из PEP 328:
Относительный импорт использует атрибут имени модуля для определения позиции этого модуля в иерархии пакетов. Если имя модуля не содержит никакой информации о пакете (например, для него установлено значение " main "), то относительный импорт разрешается, как если бы модуль был модулем верхнего уровня, независимо от того, где модуль фактически расположен в файловой системе.
Относительный импорт работает от имени файла (__name__
атрибут), который может принимать два значения:
- Это имя файла, которому предшествует структура папки, разделенная точками. Например:
package.test_A.test
Здесь Python знает родительские каталоги: beforetest
приходитtest_A
а потомpackage
. Таким образом, вы можете использовать точечную нотацию для относительного импорта.
# package.test_A/test.py
from ..A import foo
Затем у вас может быть корневой файл в корневом каталоге, который вызывает
test.py
:
# root.py
from package.test_A import test
- При запуске модуля (
test.py
) напрямую, он становится точкой входа в программу, поэтому__name__
==__main__
. Имя файла не имеет указания на структуру каталогов, поэтому Python не знает, как подняться в каталоге. Для Pythontest.py
становится скриптом верхнего уровня, над ним ничего нет. Вот почему вы не можете использовать относительный импорт.
- Возможные решения
A) Один из способов решить эту проблему - иметь корневой файл (в корневом каталоге), который вызывает модули / пакеты, например:
-
root.py
импортtest.py
. (точка входа,__name__ == __main__
). -
test.py
(относительный) импортfoo.py
. -
foo.py
говорит, что модуль был импортирован.
Результат:
package.A.foo has been imported
Module's name is: package.test_A.test
Б) Если вы хотите выполнить код как модуль, а не как сценарий верхнего уровня, вы можете попробовать это из командной строки:
python -m package.test_A.test
Любые предложения приветствуются.
Вы также должны проверить: относительный импорт в миллиардный раз, особенно ответ BrenBarn.
from package.A import foo
Я думаю, что это яснее, чем
import sys
sys.path.append("..")
Как предлагает самый популярный ответ, в основном это потому, что ваш PYTHONPATH
или же sys.path
включает в себя .
но не ваш путь к вашей посылке. И относительный импорт относится к вашему текущему рабочему каталогу, а не к файлу, в который происходит импорт; как ни странно.
Вы можете исправить это, сначала изменив относительный импорт на абсолютный, а затем либо начав с:
PYTHONPATH=/path/to/package python -m test_A.test
ИЛИ форсировать путь Python при вызове таким образом, потому что:
С python -m test_A.test
ты исполняешь test_A/test.py
с __name__ == '__main__'
а также __file__ == '/absolute/path/to/test_A/test.py'
Это означает, что в test.py
Вы могли бы использовать свой абсолют import
частично защищены в основном случае, а также выполняют одноразовые манипуляции с путями Python:
from os import path
…
def main():
…
if __name__ == '__main__':
import sys
sys.path.append(path.join(path.dirname(__file__), '..'))
from A import foo
exit(main())
На самом деле это намного проще, чем то, о чем говорят другие ответы.
TL;DR: Импорт напрямую вместо попытки относительного импорта.
Текущий рабочий каталог не является пакетом, если вы не импортируете папку из другой папки. Таким образом, поведение вашего пакета будет работать нормально, если вы намереваетесь импортировать его другими приложениями. Что не работает, так это тесты ...
Не меняя ничего в вашей структуре каталогов, все, что нужно изменить, - это то, как
test.py
импорт
foo.py
.
from A import foo
Сейчас работает
python -m test_A.test
от
package
каталог будет работать без
ImportError
.
Почему это работает?
Ваш текущий рабочий каталог не пакет, но он будет добавлен в пути. Поэтому вы можете импортировать папку
A
и его содержимое напрямую. По той же причине вы можете импортировать любой другой пакет, который вы установили ... все они включены в ваш путь.
Если кто-то все еще испытывает некоторые затруднения после уже предоставленных великолепных ответов, подумайте об этом:
Основная цитата с вышеуказанного сайта:
"То же самое можно указать программно следующим образом:
импорт системы
sys.path.append('..')
Конечно, код выше должен быть написан перед другим оператором импорта .
Совершенно очевидно, что так и должно быть, думать об этом после факта. Я пытался использовать sys.path.append('..') в своих тестах, но столкнулся с проблемой, опубликованной OP. Добавив определение import и sys.path до других моих импортов, я смог решить эту проблему.
Просто удалите
..
в test.py Для меня pytest отлично работает с этим
примером:
from A import foo
Если у вас есть "_ _init__.py" в верхней папке, вы можете инициализировать импорт как файл / путь импорта в качестве псевдонима в этом файле инициализации, а затем вы можете просто использовать его в нижних сценариях как:
импортировать псевдоним
В моем случае мне пришлось изменить это: Решение 1(лучше, которое зависит от текущего пути к файлу py. Простота развертывания) Используйте pathlib.Path.parents, чтобы сделать код более чистым
import sys
import os
import pathlib
target_path = pathlib.Path(os.path.abspath(__file__)).parents[3]
sys.path.append(target_path)
from utils import MultiFileAllowed
Решение 2
import sys
import os
sys.path.append(os.getcwd())
from utils import MultiFileAllowed
По моему скромному мнению, я так понимаю этот вопрос:
[СЛУЧАЙ 1] Когда вы запускаете абсолютный импорт, например
python -m test_A.test
или
import test_A.test
или
from test_A import test
вы фактически устанавливаете привязку импорта кtest_A
, другими словами, пакет верхнего уровня test_A
. Итак, когда у нас есть test.py, сделайтеfrom ..A import xxx
, вы ускользаете от привязки, а Python этого не допускает.
[СЛУЧАЙ 2] Когда вы это сделаете
python -m package.test_A.test
или
from package.test_A import test
ваш якорь становится package
, так package/test_A/test.py
делать from ..A import xxx
не выходит из якоря (все еще внутри package
папка), и Python с радостью принимает это.
Коротко:
- Абсолютный импорт изменяет текущую привязку (= переопределяет, что является пакетом верхнего уровня);
- Относительный импорт не изменяет привязку, а ограничивается ею.
Кроме того, мы можем использовать полное имя модуля(FQMN) для проверки этой проблемы.
Проверяйте FQMN в каждом случае:
- [CASE2]
test.__name__
знак равноpackage.test_A.test
- [СЛУЧАЙ 1]
test.__name__
знак равноtest_A.test
Итак, для CASE 2 from .. import xxx
приведет к созданию нового модуля с FQMN =package.xxx
, что приемлемо.
В то время как для CASE 1 ..
изнутри from .. import xxx
выпрыгнет из начального узла(якоря)test_A
, и это НЕ допускается Python.
Не уверен в python 2.x, но в python 3.6, если вы пытаетесь запустить весь пакет, вам просто нужно использовать -t
-t, --top-level-directory каталог Каталог верхнего уровня проекта (по умолчанию начальный каталог)
Итак, на структуре вроде
project_root
|
|----- my_module
| \
| \_____ my_class.py
|
\ tests
\___ test_my_func.py
Например, можно использовать:
python3 unittest discover -s /full_path/project_root/tests -t /full_path/project_root/
И все же импортировать my_module.my_class
без серьезных драм.
Имея
package/
__init__.py
A/
__init__.py
foo.py
test_A/
__init__.py
test.py
в
A/__init__.py
Импортировать
foo
:
from .foo import foo
при импорте
A/
из
test_A/
import sys, os
sys.path.append(os.path.abspath('../A'))
# then import foo
import foo
У меня это не сработало, поскольку я использую Django 2.1.3:
import sys
sys.path.append("..") # Adds higher directory to python modules path.
Я выбрал собственное решение, в котором я добавил команду к сценарию запуска сервера, чтобы скопировать мой общий сценарий в приложение django, которому нужен общий сценарий python. Это не идеально, но поскольку я только разрабатываю личный веб-сайт, он мне подходит. Я опубликую здесь еще раз, если найду способ разделения кода между приложениями Django с помощью django на одном веб-сайте.