Імпорт пакетів


200

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

З такою структурою:

├── LICENSE.md
├── README.md
├── api
   ├── __init__.py
   ├── api.py
   └── api_key.py
├── examples
   ├── __init__.py
   ├── example_one.py
   └── example_two.py
└── tests
   ├── __init__.py
   └── test_one.py

Як скрипти в імпортах examplesта testsкаталогах можуть імпортуватися з apiмодуля та запускатися з командного рядка?

Також я хотів би уникнути некрасивого sys.path.insertзлому для кожного файлу. Звичайно, це можна зробити в Python, правда?


7
Я рекомендую пропустити повз усі sys.pathхаки та прочитати єдине фактичне рішення, яке було розміщено дотепер (через 7 років!).
Аран-Фей

1
До речі, є ще місце для іншого хорошого рішення: відокремлення виконуваного коду від коду бібліотеки; більшість випадків сценарій всередині пакета не повинен бути виконаним для початку.
Аран-Фей

Це так корисно, як питання, так і відповіді. Мені просто цікаво, чому так, що "Прийнятий відповідь" - це не те саме, що в цій справі присудили нагороду?
Indominus

@ Aran-Fey Це недооцінене нагадування у цих відносних помилках імпорту Q & As. Я весь цей час шукав злому, але глибоко в глибині душі я знав, що існує простий спосіб створити свій вихід із проблеми. Не кажучи про те, що це рішення для всіх, хто тут читає, але це гарне нагадування, як може бути для багатьох.
колорит

Відповіді:


70

Через сім років

Оскільки я написав відповідь нижче, зміни sys.pathвсе ще є швидким і брудним трюком, який добре працює для приватних сценаріїв, але було вдосконалено декілька

  • Встановлення пакету (у virtualenv чи ні) дасть вам те, що ви хочете, хоча я б запропонував використовувати pip для цього, а не безпосередньо використовувати setuptools (і використовувати setup.cfgдля зберігання метаданих)
  • Використання -mпрапора та запуску в якості пакету також працює (але вийде трохи незручно, якщо ви хочете перетворити робочий каталог у встановлений пакет).
  • Для тестів, зокрема, pytest в цій ситуації може знайти пакет api та піклується про sys.pathхаки для вас

Отже, це дійсно залежить від того, що ви хочете зробити. У вашому випадку, однак, оскільки, здається, ваша мета - зробити належний пакет в якийсь момент, встановлення через pip -e- це, мабуть, найкраща ставка, навіть якщо вона ще не ідеальна.

Стара відповідь

Як вже було сказано в іншому місці, жахлива правда полягає в тому, що вам потрібно робити некрасиві хаки, щоб дозволити імпорт з модулів побратимів або пакетів батьків з __main__модуля. Питання детально описано в PEP 366 . PEP 3122 намагався впоратися з імпортом більш раціонально, але Гвідо відкинув його з одного рахунку

Єдиним випадком використання, здається, є запущені сценарії, які, як правило, живуть у каталозі модуля, який я завжди сприймав як антипатент.

( тут )

Хоча, я регулярно користуюся цією схемою

# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
    from sys import path
    from os.path import dirname as dir

    path.append(dir(path[0]))
    __package__ = "examples"

import api

Ось path[0]батьківська папка запущеного сценарію та папка dir(path[0])верхнього рівня.

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


3
вам не доведеться, якщо ви запускаєте з каталогу проекту за допомогою -mформи або встановлюєте пакет (pip і virtualenv полегшують це)
jfs

2
Як pytest знаходить пакет api для вас? Приємно, що я знайшов цю тему, тому що я стикаюся з цією проблемою спеціально з імпортом пакету pytest та sibling.
JuniorIncanter

1
У мене є два питання, будь ласка. 1. Здається, ваша модель працює __package__ = "examples"для мене. Для чого ти його використовуєш? 2. У якій ситуації є, __name__ == "__main__"але __package__ні None?
actual_panda

@actual_panda Налаштування __packages__допомагає, якщо ви хочете абсолютного шляху, наприклад, examples.apiдо роботи iirc (але це було давно, коли я останній раз це робив), а перевірка цього пакету - ні.
Євпок

Боже, якщо тільки інші мови зробили б той самий процес таким же простим, як і в Python. Я бачу, чому всі люблять цю мову. До речі, документація також відмінна. Я люблю витягувати типи повернення з неструктурованого тексту, це приємна зміна від Javadoc та phpdoc. ffs ....
мат

168

Набридло хаків sys.path?

Доступно багато- sys.path.appendхаків, але я знайшов альтернативний спосіб вирішення проблеми.

Підсумок

  • Загорніть код в одну папку (наприклад packaged_stuff)
  • Скористайтеся setup.pyсценарієм створення, де ви використовуєте setuptools.setup () .
  • Pip встановіть пакет у редагованому стані за допомогою pip install -e <myproject_folder>
  • Імпортувати за допомогою from packaged_stuff.modulename import function_name

Налаштування

Початковою точкою є надана вами файлова структура, загорнута у папку з назвою myproject.

.
└── myproject
    ├── api
       ├── api_key.py
       ├── api.py
       └── __init__.py
    ├── examples
       ├── example_one.py
       ├── example_two.py
       └── __init__.py
    ├── LICENCE.md
    ├── README.md
    └── tests
        ├── __init__.py
        └── test_one.py

Я зателефоную в .кореневу папку, і в моєму прикладі вона знаходиться за адресою C:\tmp\test_imports\.

api.py

В якості тестового випадку скористаємося наступним ./api/api.py

def function_from_api():
    return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

Спробуйте запустити test_one:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\myproject\tests\test_one.py", line 1, in <module>
    from api.api import function_from_api
ModuleNotFoundError: No module named 'api'

Також намагаються відносний імпорт не працювати:

Використання from ..api.api import function_from_apiпризведе до

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\tests\test_one.py", line 1, in <module>
    from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package

Кроки

  1. Зробіть файл setup.py в каталог кореневого рівня

Вміст для setup.pyбуде *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Використовуйте віртуальне середовище

Якщо ви знайомі з віртуальними середовищами, активуйте одне і перейдіть до наступного кроку. Використання віртуальних середовищ не є абсолютно необхідним, але вони дійсно допоможуть вам у довгостроковій перспективі (коли у вас більше 1 проекту, який триває ..). Найбільш основні кроки - це запуск у кореневій папці

  • Створити віртуальне оточення
    • python -m venv venv
  • Активувати віртуальне оточення
    • source ./venv/bin/activate(Linux, macOS) або ./venv/Scripts/activate(Win)

Щоб дізнатись більше про це, просто Google видає "навчальний посібник python virtual env" або подібне. Напевно, вам ніколи не потрібні інші команди, крім створення, активації та деактивації.

Після того як ви створили та активували віртуальне середовище, ваша консоль повинна вказати назву віртуальної середовища в дужках

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

і дерево папок має виглядати так **

.
├── myproject
   ├── api
      ├── api_key.py
      ├── api.py
      └── __init__.py
   ├── examples
      ├── example_one.py
      ├── example_two.py
      └── __init__.py
   ├── LICENCE.md
   ├── README.md
   └── tests
       ├── __init__.py
       └── test_one.py
├── setup.py
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. pip встановити проект у редагованому стані

Встановіть пакет верхнього рівня myprojectза допомогою pip. Хитрість полягає у використанні -eпрапора під час встановлення. Таким чином він встановлюється в редагованому стані, і всі зміни, внесені до файлів .py, будуть автоматично включені до встановленого пакету.

У кореневому каталозі запустіть

pip install -e . (зверніть увагу на крапку, вона означає "поточний каталог")

Ви також можете бачити, що він встановлений за допомогою pip freeze

(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
  Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
  1. Додайте myproject.до свого імпорту

Зауважте, що вам доведеться додавати myproject.лише до імпорту, який би не працював інакше. Імпорт, який працював без setup.py&, pip installбуде працювати добре. Дивіться приклад нижче.


Випробування розчину

Тепер давайте перевіримо рішення, використовуючи api.pyвизначені вище та test_one.pyвизначені нижче.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

запуск тесту

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

* Докладніші приклади setup.py див. У документах setuptools.

** Насправді ви можете розмістити своє віртуальне середовище де завгодно на жорсткому диску.


13
Дякуємо за детальну публікацію. Ось моя проблема. Якщо я зроблю все, що ви сказали, і я заморожую піп, я отримую рядок -e git+https://username@bitbucket.org/folder/myproject.git@f65466656XXXXX#egg=myprojectБудь-яка ідея, як вирішити?
Сі Пн

2
Чому рішення щодо відносного імпорту не працює? Я вам вірю, але я намагаюся зрозуміти згорнуту систему Python.
Джаред Нільсен

8
У когось є проблеми щодо ModuleNotFoundError? Я встановив "мій проект" у virtualenv, виконуючи ці кроки, і коли я входжу в інтерпретований сеанс і запускаю, import myprojectотримую ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectпоказує, що саме там каталог є правильним, і обидві версії pipта pythonперевіряються правильними.
ThatKind

2
Привіт @ np8, він працює, я випадково встановив його у venv та в os :) pip listпоказує пакунки, в той час як pip freezeпоказує дивні назви, якщо встановлений із прапором -e
Grzegorz Krug

3
Провели близько 2 годин, намагаючись зрозуміти, як змусити відносний імпорт працювати, і ця відповідь була тим, що нарешті насправді зробило щось розумне. 👍👍
Грем Леа

43

Ось ще одна альтернатива, яку я вставляю у верхній частині файлів Python у testsпапці:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))

1
+1 дуже просто, і він працював чудово. Вам потрібно додати батьківський клас до імпорту (наприклад, api.api, example.example_two), але я віддаю перевагу саме цьому.
Еван Плейс

10
Я думаю, що новачкам (як я) варто згадати, що ..тут відносно каталогу, з якого ви виконуєте ---, а не з каталогу, що містить цей тестовий / прикладний файл. Я виконую з каталогу проектів, і мені це потрібно було ./. Сподіваюсь, це допомагає комусь іншому.
Джошуа Детвейлер

@JoshDetwiler, так абсолютно. Я цього не знав. Дякую.
док

1
Це погана відповідь. Зламувати шлях не є хорошою практикою; скандально, наскільки він використовується у світі пітонів. Одним із головних моментів цього питання було бачити, як можна здійснити імпорт, уникаючи такого роду злому.
jtcotton63

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc

31

Вам не потрібно і не слід рубати, sys.pathякщо це не потрібно, і в цьому випадку це не так. Використання:

import api.api_key # in tests, examples

Запуск з директорії проекту: python -m tests.test_one.

Ви, ймовірно, повинні рухатись tests(якщо вони є єдиними тестами api) всередині apiі бігати, python -m api.testщоб запустити всі тести (якщо припустити, що є __main__.py) або замість цього python -m api.test.test_oneзапустити test_one.

Ви також можете видалити __init__.pyз examplesнього (це не пакет Python) та запустити приклади у virtualenv, де apiвін встановлений, наприклад, pip install -e .у virtualenv буде встановлений apiпакет inplace, якщо у вас є належний setup.py.


@Alex у відповіді не передбачає, що тести - це тести API, за винятком абзацу, де прямо написано "якщо вони є єдиними тестами api" .
jfs

на жаль, тоді ви застрягли в запуску з root dir, і PyCharm досі не знаходить файл для його приємних функцій
mhstnsc

@mhstnsc: це невірно. Ви повинні мати можливість бігати python -m api.test.test_oneз будь-якого місця, коли активовано virtualenv. Якщо ви не можете налаштувати PyCharm для запуску тестів, спробуйте задати нове запитання щодо переповнення стека (якщо ви не можете знайти існуюче запитання з цієї теми).
jfs

@jfs Я пропустив віртуальний шлях до env, але я не хочу використовувати щось більше, ніж рядок shebang для запуску цього матеріалу з будь-якого каталогу. Справа не в тому, щоб працювати з PyCharm. Робочі розробники з PyCharm знали б також, що вони мають завершення та стрибки через функції, які я не міг би змусити його працювати з будь-яким рішенням.
mhstnsc

@mhstnsc у багатьох випадках достатній відповідний шебанг (вкажіть його на бінарний файл virtualenv python. Будь-який гідний Python IDE повинен підтримувати virtualenv.
jfs

9

У мене ще немає розуміння Pythonology, необхідного для того, щоб побачити призначений спосіб спільного використання коду між спорідненими проектами без злому / відносного імпорту. До цього дня це моє рішення. Для того, щоб examplesабо testsімпортувати речі ..\api, це виглядатиме так:

import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key

Це все одно надасть вам батьківський каталог api, і вам не знадобиться "/ .." конкатенація sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( файл ))) )
Каміло Санчес

4

Для імпорту пакетів побратимів, ви можете використовувати або вставити, або додавати метод модуля [sys.path] [2] :

if __name__ == '__main__' and if __package__ is None:
    import sys
    from os import path
    sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
    import api

Це спрацює, якщо ви запускаєте свої сценарії наступним чином:

python examples/example_one.py
python tests/test_one.py

З іншого боку, ви також можете використовувати відносний імпорт:

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api

У цьому випадку вам доведеться запустити свій скрипт аргументом '-m' (зауважте, що в цьому випадку ви не повинні давати розширення '.py' ):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

Звичайно, ви можете змішати два підходи, щоб ваш сценарій працював незалежно від того, як він називається:

if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
        import api
    else:
        import ..api.api

Я використовував рамку Click, яка не має __file__глобальної sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))
версії,

3

TLDR

Цей метод не вимагає setuptools, взломів шляхів, додаткових аргументів командного рядка або вказівки верхнього рівня пакету у кожному файлі вашого проекту.

Просто зробіть скрипт у батьківському каталозі будь-якого, хто вас закликає бути вашим, __main__і запустіть усе звідти. Для подальшого пояснення продовжуйте читати.

Пояснення

Це можна досягти без злому нового шляху разом, додаткових аргументів командного рядка або додавання коду до кожної програми, щоб розпізнати її побратимів.

Причиною цього не вдалося, як я вважаю, згадувалося раніше, це те, що програми, що викликаються, мають свій __name__набір__main__ . У цьому випадку викликаний скрипт приймає себе на найвищому рівні пакету і відмовляється розпізнавати сценарії в каталогах братів і сестер.

Однак усе, що знаходиться під верхнім рівнем каталогу, все одно розпізнає ВСІЙ ЕЛЬШЕ під верхнім рівнем. Це означає, що ТІЛЬКО, що вам потрібно зробити, щоб отримати файли в каталогів братів, які розпізнають / використовують один одного, - це викликати їх зі скрипту в їхньому батьківському каталозі.

Доказ концепції в режимі з такою структурою:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py містить наступний код:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py містить:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

а sib2 / callsib.py містить:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

Якщо ви відтворите цей приклад, ви помітите, що виклик Main.pyпризведе до того, що "Got Called" буде надруковано так, як визначено в ході, через sib2/callsib.py те, що вас sib2/callsib.pyвикликали sib1/call.py. Однак якщо потрібно було безпосередньо телефонувати sib1/call.py(після внесення відповідних змін до імпорту), це викидає виняток. Незважаючи на те, що він працював, коли його викликав сценарій у своєму батьківському каталозі, він не працюватиме, якщо він вважає, що знаходиться на найвищому рівні пакету.


2

Я зробив зразковий проект, щоб продемонструвати, як я впорався з цим, що насправді є іншим хак системою sys.path, як зазначено вище. Приклад імпорту з братами Python , який спирається на:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

Це здається досить ефективним, доки ваш робочий каталог залишається в корені проекту Python. Якщо хтось розгортає це в реальному виробничому середовищі, було б чутно почути, чи працює він і там.


Це працює лише в тому випадку, якщо ви працюєте з батьківського каталогу
скрипту

1

Вам потрібно подивитися, щоб записати заяви про імпорт у відповідний код. Якщо examples/example_one.pyвикористовується такий заява про імпорт:

import api.api

... тоді він очікує, що кореневий каталог проекту буде в системному шляху.

Найпростіший спосіб підтримати це без будь-яких злому (як ви це сказали) - це запустити приклади з каталогу верхнього рівня, як це:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 

В Python 2.7.1 я отримую наступне: $ python examples/example.py Traceback (most recent call last): File "examples/example.py", line 3, in <module> from api.api import API ImportError: No module named api.api. Я теж отримую те саме з import api.api.
zachwill

Оновлений моя відповідь ... Ви дійсно повинні додати поточну директорію на шляху імпорту, жодним чином навколо цього.
AJ.

1

На всякий випадок, якщо хтось, хто використовує Pydev на Eclipse, закінчився тут: ви можете додати батьківський шлях рідних братів (і, таким чином, батьківський модуль виклику), як зовнішню папку бібліотеки, використовуючи Project-> Properties і встановивши Зовнішні бібліотеки в лівому меню Pydev-PYTHONPATH . Потім ви можете імпортувати зі свого брата, напр from sibling import some_class.


-3

По-перше, вам слід уникати файлів з тим же ім'ям, що і сам модуль. Це може порушити інший імпорт.

Коли ви імпортуєте файл, перекладач спочатку перевіряє поточний каталог, а потім шукає глобальні каталоги.

Всередині examplesабо testsви можете зателефонувати:

from ..api import api

Я отримую наступне з Python 2.7.1:Traceback (most recent call last): File "example_one.py", line 3, in <module> from ..api import api ValueError: Attempted relative import in non-package
zachwill

2
О, тоді вам слід додати __init__.pyфайл до каталогу верхнього рівня. Інакше Python не може сприймати його як модуль

8
Це не спрацює. Проблема полягає не в тому, що батьківська папка не є пакетом, це те, що оскільки модуль __name__знаходиться __main__замість package.module, Python не може бачити його батьківський пакет, тому .вказує ні на що.
Євпок
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.