Импортировать локальную функцию из модуля, размещенного в другом каталоге, с относительным импортом в Jupyter Notebook с использованием Python 3

127

У меня есть структура каталогов, подобная следующей

meta_project
    project1
        __init__.py
        lib
            module.py
            __init__.py
    notebook_folder
        notebook.jpynb

При работе в notebook.jpynbпри попытке использовать относительный импорт , чтобы получить доступ к функции function()в module.pyс:

from ..project1.lib.module import function

Я получаю следующую ошибку:

SystemError                               Traceback (most recent call last)
<ipython-input-7-6393744d93ab> in <module>()
----> 1 from ..project1.lib.module import function

SystemError: Parent module '' not loaded, cannot perform relative import

Есть ли способ заставить это работать, используя относительный импорт?

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

Также обратите внимание, что, по крайней мере, как изначально предполагалось, project1он не рассматривался как модуль и, следовательно, не имеет __init__.pyфайла, он просто предназначен как каталог файловой системы. Если решение проблемы требует рассматривать его как модуль и включать __init__.pyфайл (даже пустой), это нормально, но этого недостаточно для решения проблемы.

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


Изменить: это отличается от относительного импорта в Python 3 , в котором говорится об относительном импорте в Python 3 в целом и, в частности, о запуске сценария из каталога пакета. Это связано с работой в записной книжке jupyter, пытающейся вызвать функцию в локальном модуле в другом каталоге, который имеет как разные общие, так и частные аспекты.

mpacer
источник
1
есть ли __init__файлы в каталоге вашего пакета?
Iron Fist
Да, в libсправочнике.
mpacer
Пожалуйста, укажите это в своей структуре каталогов в своем вопросе
Iron Fist
Сделал это редактирование, как только увидел ваш первый комментарий :). Спасибо, что уловили это.
mpacer
Возможный дубликат относительного импорта в Python 3
baldr

Ответы:

174

У меня был почти такой же пример, как и у вас в этой записной книжке, где я хотел проиллюстрировать использование функции соседнего модуля в СУХОМ стиле.

Мое решение состояло в том, чтобы сообщить Python об этом дополнительном пути импорта модуля, добавив в блокнот такой фрагмент:

import os
import sys
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
    sys.path.append(module_path)

Это позволяет вам импортировать желаемую функцию из иерархии модулей:

from project1.lib.module import function
# use the function normally
function(...)

Обратите внимание, что необходимо добавить пустые __init__.pyфайлы в папки project1 / и lib /, если у вас их еще нет.

metakermit
источник
6
Это решает проблему возможности импортировать пакет, используя более или менее относительное местоположение, но только косвенно. Я знаю, что Маттиас Буссонье (@matt на SE) и Yuvi Panda (@yuvi на SE) разрабатывают github.com/ipython/ipynb, который будет решать эту проблему более напрямую (например, разрешая относительный импорт с использованием стандартного синтаксиса после того, как их пакет импортируется). Я пока приму ваш ответ, и когда их решение будет полностью готово для использования другими, я, вероятно, либо напишу ответ о том, как его использовать, либо попрошу одного из них сделать это.
mpacer
спасибо, что указали на пустой init .py. Я новичок в Python, и у меня возникли проблемы с импортом моих классов. Я получал обнаруженные ошибки примечания к модулю, добавление пустого файла init .py устранило проблему!
Пэт Грэди
5
Пустой файл init .py больше не нужен в Python 3.
CathyQian
К вашему сведению: есть программа для просмотра записной книжки: nbviewer.jupyter.org/github/qPRC/qPRC/blob/master/notebook/…
thoroc
26

Пришел сюда в поисках лучших практик абстрагирования кода до подмодулей при работе в Notebooks. Я не уверен, что есть лучшая практика. Я предлагал это.

Иерархия проекта как таковая:

├── ipynb
   ├── 20170609-Examine_Database_Requirements.ipynb
   └── 20170609-Initial_Database_Connection.ipynb
└── lib
    ├── __init__.py
    └── postgres.py

И от 20170609-Initial_Database_Connection.ipynb:

    In [1]: cd ..

    In [2]: from lib.postgres import database_connection

Это работает, потому что по умолчанию Jupyter Notebook может анализировать cdкоманду. Обратите внимание, что это не использует магию Python Notebook. Он просто работает без добавления %bash.

Учитывая , что в 99 раз из 100 , я работаю в Докер с помощью одного из изображений проекта Jupyter Docker , следующая модификация является идемпотентная

    In [1]: cd /home/jovyan

    In [2]: from lib.postgres import database_connection
Джошуа Кук
источник
Спасибо. Действительно ужасные ограничения этого относительного импорта.
Майкл
Я тоже использую, chdirа не добавляю в путь, так как я заинтересован в импорте из основного репо, а также в взаимодействии с некоторыми файлами там.
TheGrimmScientist
К сожалению, это самая взломанная вещь, которую я делаю в Python. Тем не менее, я не могу найти лучшего решения.
TheGrimmScientist
для простой идемпотентности (позволяя одной и той же ячейке запускаться несколько раз и получать тот же результат) if os.path.isdir('../lib/'): os.chdir('../lib'):; или, лучше, используйте ../lib/db/с вашим, postgres.pyчтобы случайно не попасть в каталог более высокого уровня, также содержащий другой lib.
Майкл
1
Мне нравится это решение, пока я случайно не выполнил его cd ..дважды.
minhle_r7
15

Пока что принятый ответ работал у меня лучше всего. Однако меня всегда беспокоило то, что существует вероятный сценарий, при котором я мог бы notebooksпреобразовать каталог в подкаталоги, что потребовало бы изменения module_pathв каждой записной книжке. Я решил добавить файл python в каждый каталог записной книжки, чтобы импортировать необходимые модули.

Таким образом, имея следующую структуру проекта:

project
|__notebooks
   |__explore
      |__ notebook1.ipynb
      |__ notebook2.ipynb
      |__ project_path.py
   |__ explain
       |__notebook1.ipynb
       |__project_path.py
|__lib
   |__ __init__.py
   |__ module.py

Я добавил файл project_path.pyв каждый подкаталог записной книжки ( notebooks/exploreи notebooks/explain). Этот файл содержит код для относительного импорта (из @metakermit):

import sys
import os

module_path = os.path.abspath(os.path.join(os.pardir, os.pardir))
if module_path not in sys.path:
    sys.path.append(module_path)

Таким образом, мне просто нужно выполнять относительный импорт внутри project_path.pyфайла, а не в записных книжках. Файлы записных книжек тогда просто нужно будет импортировать project_pathперед импортом lib. Например в 0.0-notebook.ipynb:

import project_path
import lib

Предостережение здесь в том, что отменить импорт не получится. ЭТО НЕ РАБОТАЕТ:

import lib
import project_path

Таким образом, при импорте необходимо соблюдать осторожность.

Gerges
источник
3

Я только что нашел это красивое решение:

import sys; sys.path.insert(0, '..') # add parent folder path where lib folder is
import lib.store_load # store_load is a file on my library folder

Вам просто нужны некоторые функции этого файла

from lib.store_load import your_function_name

Если версия python> = 3.3 вам не нужен файл init.py в папке

Виктор Каллехас
источник
3
Я нашел это очень полезным. Добавлю, что следует добавить следующую модификацию ->if ".." not in sys.path: ... sys.path.insert(0,"..")
Яков Бресслер
2

Изучив эту тему самостоятельно и прочитав ответы, я рекомендую использовать библиотеку path.py, поскольку она предоставляет диспетчер контекста для изменения текущего рабочего каталога.

Тогда у вас будет что-то вроде

import path
if path.Path('../lib').isdir():
    with path.Path('..'):
        import lib

Хотя можно просто опустить isdirзаявление.

Здесь я добавлю операторы печати, чтобы упростить отслеживание происходящего

import path
import pandas

print(path.Path.getcwd())
print(path.Path('../lib').isdir())
if path.Path('../lib').isdir():
    with path.Path('..'):
        print(path.Path.getcwd())
        import lib
        print('Success!')
print(path.Path.getcwd())

который выводит в этом примере (где lib находится в /home/jovyan/shared/notebooks/by-team/data-vis/demos/lib):

/home/jovyan/shared/notebooks/by-team/data-vis/demos/custom-chart
/home/jovyan/shared/notebooks/by-team/data-vis/demos
/home/jovyan/shared/notebooks/by-team/data-vis/demos/custom-chart

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

marr75
источник
Это не будет работать в сочетании с% autoreload, поскольку путь к модулю не будет найден во время перезагрузки
Йоханнес,
1

Вот мои 2 цента:

import sys

сопоставьте путь, по которому расположен файл модуля. В моем случае это был рабочий стол

sys.path.append ( '/ Users / John / Desktop')

Либо импортируйте весь модуль сопоставления, НО тогда вы должны использовать .notation для сопоставления классов, таких как mapping.Shipping ()

import mapping # mapping.py - это имя моего файла модуля

shipit = mapping.Shipment () #Shipment - это имя класса, который мне нужно использовать в модуле сопоставления.

Или импортируйте конкретный класс из модуля сопоставления

из сопоставления импорт карт

shipit = Shipment () # Теперь не нужно использовать .notation

Бобби
источник
0

Я обнаружил, что python-dotenv помогает довольно эффективно решить эту проблему. Структура вашего проекта немного изменится, но код в вашей записной книжке станет немного проще и единообразнее для разных ноутбуков.

Для вашего проекта выполните небольшую установку.

pipenv install python-dotenv

Затем проект изменится на:

├── .env (this can be empty)
├── ipynb
   ├── 20170609-Examine_Database_Requirements.ipynb
   └── 20170609-Initial_Database_Connection.ipynb
└── lib
    ├── __init__.py
    └── postgres.py

И, наконец, ваш импорт изменится на:

import os
import sys

from dotenv import find_dotenv


sys.path.append(os.path.dirname(find_dotenv()))

+1 для этого пакета - ваши записные книжки могут содержать несколько каталогов. python-dotenv найдет ближайший в родительском каталоге и будет использовать его. +2 для этого подхода заключается в том, что jupyter будет загружать переменные среды из файла .env при запуске. Двойной удар.

t.perk
источник