После ошибки пакета верхнего уровня в относительном импорте

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

  1. Это имя файла, которому предшествует структура папки, разделенная точками. Например: package.test_A.test Здесь Python знает родительские каталоги: before test приходит test_A а потом package. Таким образом, вы можете использовать точечную нотацию для относительного импорта.
       #  package.test_A/test.py
from ..A import foo

Затем у вас может быть корневой файл в корневом каталоге, который вызывает test.py:

       #  root.py
from package.test_A import test
  1. При запуске модуля (test.py) напрямую, он становится точкой входа в программу, поэтому __name__ == __main__. Имя файла не имеет указания на структуру каталогов, поэтому Python не знает, как подняться в каталоге. Для Python test.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и его содержимое напрямую. По той же причине вы можете импортировать любой другой пакет, который вы установили ... все они включены в ваш путь.

Если кто-то все еще испытывает некоторые затруднения после уже предоставленных великолепных ответов, подумайте об этом:

https://www.daveoncode.com/2017/03/07/how-to-solve-python-modulenotfound-no-module-named-import-error/

Основная цитата с вышеуказанного сайта:

"То же самое можно указать программно следующим образом:

импорт системы

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 на одном веб-сайте.

Другие вопросы по тегам