使用 Python 3 从另一个目录中的模块导入本地函数,并在 Jupyter Notebook 中进行相对导入

Posted

技术标签:

【中文标题】使用 Python 3 从另一个目录中的模块导入本地函数,并在 Jupyter Notebook 中进行相对导入【英文标题】:Import local function from a module housed in another directory with relative imports in Jupyter Notebook using Python 3 【发布时间】:2016-04-01 09:50:38 【问题描述】:

我有一个类似下面的目录结构

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

notebook.jpynb 中工作时,如果我尝试使用相对导入来访问module.py 中的函数function()

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 文件(即使是空白文件)也可以,但这样做还不足以解决问题。

我在机器之间共享这个目录,相对导入允许我在任何地方使用相同的代码,而且我经常使用笔记本进行快速原型设计,因此涉及将绝对路径组合在一起的建议不太可能有帮助。


编辑:这与Relative imports in Python 3 不同,后者通常讨论 Python 3 中的相对导入,尤其是从包目录中运行脚本。这与在 jupyter notebook 中工作有关,试图在另一个目录中调用本地模块中的函数,该目录具有不同的一般和特定方面。

【问题讨论】:

Relative imports in Python 3的可能重复 如果project1 被认为是一个包,那么它也应该包括__init__ @baldr,它一般谈论 Python 3,特别是从包目录中运行脚本。这与在 jupyter notebook 中尝试调用另一个目录中模块中的函数有关。在发布此问题之前,我查看了那里的答案,如果您认为它是重复的或已经回答,请您指出您认为相关的特定答案吗? project1 不打算成为一个包,添加 __init__.py 并不能解决任何问题(返回相同的错误)。部分地,我打算传达的是目录结构和不同目录所扮演的角色(组织与功能)之间的差异。但是,如果没有它就无法工作,我可以将 __init__.py 添加到 Q 中。 并确保在所有这些更改后重新启动笔记本内核! 【参考方案1】:

我在this notebook 中有与您几乎相同的示例,我想以 DRY 方式说明相邻模块功能的用法。

我的解决方案是通过在笔记本中添加一个像这样的 sn-p 来告诉 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/ 文件夹中。

【讨论】:

这解决了能够使用或多或少的相对位置导入包的问题,​​但只是间接的。我碰巧知道 Matthias Bussonier(SE 上的@matt)和 Yuvi Panda(SE 上的@yuvi)正在开发github.com/ipython/ipynb,这将更直接地解决这个问题(例如,通过在导入包后允许使用标准语法进行相对导入)。我现在会接受你的回答,当他们的解决方案完全准备好供其他人使用时,我可能会写一个关于如何使用它的答案,或者请他们中的一个人这样做。 感谢您指出空的 init.py 我是 python 新手,无法导入我的类。我收到模块注释发现错误,添加空 init.py 解决了问题! 在 Python 3 中不再需要空的 init.py 文件。 仅供参考:有一个笔记本查看器:nbviewer.jupyter.org/github/qPRC/qPRC/blob/master/notebook/…【参考方案2】:

来这里寻找在笔记本中工作时将代码抽象为子模块的最佳实践。我不确定是否有最佳做法。我一直在提议这个。

这样的项目层次结构:

├── 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 即可工作。

考虑到 100 次中有 99 次我在 Docker 中使用Project Jupyter Docker images 之一,以下修改幂等的

    In [1]: cd /home/jovyan

    In [2]: from lib.postgres import database_connection

【讨论】:

谢谢。这种相对进口的限制真的很可怕。 我也使用 chdir 而不是添加到路径,因为我对从主仓库导入以及与那里的一些文件交互都很感兴趣。 可悲的是,我在 python 中做的最受黑客攻击的事情。然而,我找不到更好的解决方案。 对于简单的幂等性(允许同一个单元运行多次并得到相同的结果):if os.path.isdir('../lib/'): os.chdir('../lib');或者,更好的是,将../lib/db/postgres.py 一起使用,以免意外chdir 到一个更高的目录,该目录还包含另一个lib 我喜欢这个解决方案,直到我不小心执行了两次cd ..【参考方案3】:

到目前为止,接受的答案对我来说效果最好。但是,我一直担心有一种可能的情况,即我可能会将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

我在每个笔记本子目录(notebooks/explorenotebooks/explain)中添加了文件project_path.py。该文件包含相对导入的代码(来自@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 文件中进行相对导入,而不是在笔记本中。然后,笔记本文件只需要在导入 lib 之前导入 project_path。比如0.0-notebook.ipynb:

import project_path
import lib

这里需要注意的是,逆转导入是行不通的。这不起作用:

import lib
import project_path

因此在导入时必须小心。

【讨论】:

关于您的文件夹的问题。 “探索”是您进行探索性工作的地方? 'explain' 文件夹用于什么(只是想知道您的工作流程)【参考方案4】:

这里的所有其他答案都取决于在笔记本中添加代码(!)

在我看来,将特定路径硬编码到笔记本代码中是不好的做法,或者取决于位置,因为这使得以后很难重构代码。相反,我建议您在启动 Jupyter 笔记本服务器时将根项目文件夹添加到 PYTHONPATH,或者直接从项目文件夹中添加,就像这样

env PYTHONPATH=`pwd` jupyter notebook

或者如果您从其他地方启动它,请使用绝对路径

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

【讨论】:

这其实是这个问题的最佳答案。 不敢相信我必须向下滚动才能找到不涉及将某些内容硬编码到笔记本中的答案 :-( 谢谢!【参考方案5】:

我刚刚找到了这个漂亮的解决方案:

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文件

【讨论】:

我发现这很有帮助。我补充一下,应该添加以下修改 --> if ".." not in sys.path: ... sys.path.insert(0,"..")【参考方案6】:

我自己研究这个主题并阅读了我推荐使用path.py library 的答案,因为它提供了一个用于更改当前工作目录的上下文管理器。

然后你有类似的东西

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

由于该解决方案使用上下文管理器,因此无论您的内核在单元之前处于什么状态,也无论导入您的库代码引发什么异常,您都可以保证返回到之前的工作目录。

【讨论】:

这不能与 %autoreload 结合使用,因为在重新加载时找不到模块路径【参考方案7】:

这是我的 2 美分:

导入系统

映射模块文件所在的路径。就我而言,它是桌面

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

要么导入整个映射模块,但你必须使用 .notation 来映射类,如 mapping.Shipping()

import mapping #mapping.py 是我的模块文件名

shipit = mapping.Shipment() #Shipment是我需要在映射模块中使用的类的名称

或者从映射模块导入具体的类

从映射导入映射

shipit = Shipment() #现在你不必使用.notation

【讨论】:

【参考方案8】:

我发现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 文件加载环境变量。双重打击。

【讨论】:

【参考方案9】:

对于那些像我一样不理解解决方案的人,您必须根据具体问题的需要深入到目录中。此错误的解决方案:

没有名为“your_folder”的模块

我使用的笔记本位于:

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

我想导入这个笔记本:

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

我不得不通过@metakermit 将上面的解决方案修改为这个解决方案:

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

您可以检查sys.path 以获取附加目录。在此示例中,附加了这一行:

C:\\Users\\vojte\\projects

【讨论】:

【参考方案10】:

在使用cd.. 时改进@joshua-cook answer,以确保在使用run all 而不使用restart 时不会重新运行单元格并弄乱目录,请改用以下代码:

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

NOTEBOOK_INITIATED_FLAG 用作占位符,标记内核已经在运行,因此不需要更改目录。

如果您想使用 jupytext 并从父文件夹运行您的 .py 文件,这是超级复杂的样板:

import os
import sys

if 'NOTEBOOK_INITIATED_FLAG' not in globals():
    NOTEBOOK_INITIATED_FLAG = True
    
    try:
        # not in notebook
        module_path = os.path.join(os.path.dirname(__file__), os.pardir)
    except:
        # in notebook
        module_path = os.path.abspath(os.path.join('..'))
        %cd ..
        %pwd

    if module_path not in sys.path:
        sys.path.append(module_path)

【讨论】:

以上是关于使用 Python 3 从另一个目录中的模块导入本地函数,并在 Jupyter Notebook 中进行相对导入的主要内容,如果未能解决你的问题,请参考以下文章

如何检查 Python 包中的任何模块是不是从另一个包导入?

Python 3模块从另一个文件夹导入错误[重复]

从另一个目录导入的Python失败

将Python导入函数从另一个目录导入文件,然后从另一个目录导入另一个函数

无法从python中的另一个目录导入python类[重复]

如何将 python 函数从另一个文件导入 django 视图