Импортировать локальную функцию из модуля, расположенного в другом каталоге, с относительным импортом в блокнот jupyter, используя python3

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

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, пытающегося вызвать функцию в локальном модуле в другом каталоге, который имеет как общие, так и частные аспекты.

14 ответов

Решение

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

Мое решение состояло в том, чтобы сообщить 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/, если у вас их еще нет.

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

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

├── 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 может анализировать cd команда. Обратите внимание, что это не использует магию Python Notebook. Это просто работает без предварительной %bash,

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

    In [1]: cd /home/jovyan

    In [2]: from lib.postgres import database_connection

All other answers here depends on adding code the the notebook(!)

In my opinion is bad practice to hardcode a specific path into the notebook code, or otherwise depend on the location, since this makes it really hard to refactor you code later on. Instead I would recommend you to add the root project folder to PYTHONPATH when starting up your Jupyter notebook server, either directly from the project folder like so

env PYTHONPATH=`pwd` jupyter notebook

or if you are starting it up from somewhere else, use the absolute path like so

env PYTHONPATH=/Users/foo/bar/project/ jupyter notebook

До сих пор принятый ответ работал лучше всего для меня. Однако меня всегда беспокоило то, что существует вероятный сценарий, в котором я мог бы 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

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

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

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 в папке

Если вы работаете через расширение Jupyter в VS Code, я обнаружил, что теперь вы можете указать этот путь в своемsettings.jsonфайл.

      {
    "jupyter.notebookFileRoot": "/path/to/your/module/root"
}

Изменить: Или, чтобы установить его в корне вашей рабочей области в более общем виде:

      {
    "jupyter.notebookFileRoot": "${workspaceFolder}"
}

Я обнаружил, что 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 при запуске. Двойной удар.

Изучив эту тему самостоятельно и прочитав ответы, я рекомендую использовать библиотеку 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

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

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

import sys

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

sys.path.append('/ Пользователи / Джон / Рабочий стол')

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

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

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

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

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

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

Я разозлился, поэтому написал пакет .

pip install package_rootи тогда ты сможешьimport package_root # into your PYTHONPATH.

Это работает, просматривая стек вызовов, чтобы определить, какой файл импортированpackage_rootа затем пройти вверх по каталогам, чтобы найти первый каталог, в котором отсутствует файл.

Итак, на практике вам нужно будет добавить__init__.pyфайлы в каталоги вашей записной книжки, как если бы вы заставляли пакеты Python работать.

For those who don't understand the solution as I didn't, you have to go as deep to the directory as you need for your specific problem. Solution for this errorr:

No module named 'your_folder'

My notebook that I work with is in:

C:\Users\vojte\projects\predicta\EDA\apartments\EDA.ipynb

I want to import this notebook:

C:\Users\vojte\projects\functions\functions.ipynb

I had to modify the solution above by @metakermit to this solution:

      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 functions import functions as f

You can check the sys.path for appended directory.In this example this line was appended:

C:\\Users\\vojte\\projects

Вот общее решение, основанное на этом ответе , которое не требует ни указания имени родительской папки, ни изменения текущего рабочего каталога.

Просто обновитеroot_parent_levelс количеством родительских уровней, где начинается относительный импорт, и убедитесь, что__init__.pyсуществует в каждом подпакете.

      if "PKG" not in globals(): # `PKG` is used just to avoid re-excuting the cell more than once
  root_parent_level = 2
  import importlib, sys, pathlib
  PKG = %pwd
  PKG = pathlib.Path(PKG)
  root = PKG
  full_pkg = f"{root.name}"
  for _ in range(root_parent_level):
    root = root.parent
    full_pkg = f"{root.name}.{full_pkg}"
    MODULE_PATH = f"{root}{pathlib.os.path.sep}__init__.py"
    MODULE_NAME = f"{root.name}"
    spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module 
    spec.loader.exec_module(module)
  __package__ = full_pkg

Если вы используете VSCODE, вы можете включить его в настройках: Настройки VSCODE

Улучшение @ Навина кулинарной ответ при помощи cd.., чтобы убедиться, что вы не перезапустили ячейку и не испортили свой каталог при использовании run all без restart, используйте вместо этого этот код:

      if 'NOTEBOOK_INITIATED_FLAG' not in globals():
    NOTEBOOK_INITIATED_FLAG = True
    %cd ..
%pwd

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

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