在Python中如何进行相对导入?

615

想象一下这个目录结构:

app/
   __init__.py
   sub1/
      __init__.py
      mod1.py
   sub2/
      __init__.py
      mod2.py

我正在编写mod1,需要从mod2中导入一些内容。我该怎么做?

我尝试了from ..sub2 import mod2,但是出现了“在非包中尝试相对导入”的错误。

我在谷歌上搜索了一下,但只找到了"sys.path操纵"的技巧。难道没有更简单的方法吗?


我所有的__init__.py目前都是空的。

我这样做是因为sub2包含了在子包(sub1subX等)之间共享的类。

我想要的行为与PEP 366感谢John B)中描述的相同。


9
我建议您更新问题,更清晰地描述您所描述的PEP 366中提到的问题。 - John B
2
这是一个冗长的解释,但请查看此处:https://dev59.com/RG855IYBdhLWcg3wbztk#10713254 我回答了一个非常类似的问题。我也遇到了同样的问题,直到昨晚。 - Sevvy325
3
对于那些希望加载位于任意路径的模块的人,请参考此链接:https://dev59.com/anVD5IYBdhLWcg3wKYP- - Evgeni Sergeev
2
相关的是,Python 3 将默认将导入处理方式更改为默认情况下为绝对路径;相对导入必须明确指定。 - Ross
18个回答

382
问题是你通过将mod1.py作为解释器的参数来将模块运行为'__main__'。
从PEP 328中可以看到:
相对导入使用模块的__name__属性来确定该模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如设置为'__main__'),则相对导入将被解析为顶级模块,而不管模块实际上位于文件系统的哪个位置。
在Python 2.6中,他们添加了相对于主模块引用模块的能力。PEP 366描述了这个变化。

82
推荐的替代方法是使用"-m"开关在包内运行模块,而不是直接指定文件名来运行。 - ncoghlan
163
我不明白:这里的答案在哪里?怎样才能在这样的目录结构中导入模块? - Tom
31
在这种情况下,mod1会使用from sub2 import mod2。然后,在应用程序内部运行mod1需要执行python -m sub1.mod1 - Xiong Chiamiov
12
这是否意味着,如果你的Python嵌入在一个应用程序中,因此无法访问Python命令行开关,你就无法这样做? - LarsH
7
每个人似乎都想告诉你应该做什么,而不是直接回答问题。好的,这正是写软件寻求帮助应该有的方式!当我违反良好实践时,我希望知道。 - jpmc26
显示剩余5条评论

161

下面是我用的解决方案:

我使用相对导入,例如 from ..sub2 import mod2 然后,如果我想运行mod1.py,则进入app的父目录,并使用python -m开关运行模块,如python -m app.sub1.mod1.

相对导入出现问题的真正原因是它通过获取模块的__name__属性来工作。如果直接运行模块,则会将__name__设置为__main__,并且不包含任何有关包结构的信息。这就是Python抱怨relative import in non-package错误的原因。

因此,使用-m开关可以向Python提供包结构信息,从而成功解析相对导入。

在进行相对导入时,我遇到了这个问题很多次。在阅读了所有先前的答案之后,我仍然无法找出如何以干净的方式解决它,而无需在所有文件中添加样板代码。(尽管某些评论确实很有帮助,感谢@ncoghlan 和@XiongChiamiov)

希望这能帮助正在遭受相对导入问题的人,因为阅读PEP并不是一件有趣的事情。


16
最佳答案我认为:不仅解释了为什么问题出现,还找到了一种解决方法,不改变模块导入方式。毕竟,OP的相对导入是正确的,罪魁祸首是直接作为脚本运行时无法访问外部包,这正是-m命令旨在解决的。 - MestreLion
30
请注意:这个回答是在问题提出五年后作出的。当时还没有这些功能。 - JeremyKun
2
如果你想从同一目录导入一个模块,可以使用 from . import some_module - Rotareti
这是帮助了我并且让我梳理思路的答案:**为了运行包含相对导入的 Python 脚本,必须在 $ PWD 是其父目录的情况下将脚本作为模块运行,例如 $ python -m app.main**。为了清晰起见,应该使用 $ python -m <main_directory>.<script_with_relative_imports> - Jesse H.

131
main.py
setup.py
app/ ->
    __init__.py
    package_a/ ->
       __init__.py
       module_a.py
    package_b/ ->
       __init__.py
       module_b.py
  1. 运行 python main.py
  2. main.py 执行: import app.package_a.module_a
  3. module_a.py 执行:import app.package_b.module_b

或者,2 或 3 可以使用:from app.package_a import module_a

只要在 PYTHONPATH 中有 app 即可。这样,main.py 可以放置在任何位置。

因此,您可以编写一个 setup.py 文件将整个应用程序包和子包复制(安装)到目标系统的 Python 文件夹中,将 main.py 复制到目标系统的脚本文件夹中。


3
很好的回答。有没有一种方法可以在不安装到PYTHONPATH的情况下导入这种方式? - auraham
4
建议阅读:http://blog.habnab.it/blog/2013/07/21/python-packages-and-you/ - nosklo
17
有一天,需要将应用程序的名称更改为test_app。会发生什么?您需要更改所有源代码,导入app.package_b.module_b --> test_app.package_b.module_b。这是非常糟糕的做法...我们应该尝试在包内使用相对导入。 - Spybdai

55

"Guido认为在包内运行脚本是一种反模式"(被拒绝的PEP-3122

我花了很多时间寻找解决方案,阅读了Stack Overflow上相关的帖子,并对自己说“一定有更好的方法!”。看起来并没有。


10
注意:已经提到[pep-366](http://python.org/dev/peps/pep-0366/)(与[pep-3122](http://www.python.org/dev/peps/pep-3122/)同时创建)提供了相同的功能,但使用不同的向后兼容实现,即,如果你想在包中运行一个模块作为脚本*并且*在其中使用显式相对导入,则可以使用`-m`开关来运行它:`python -m app.sub1.mod1或从顶级脚本中调用app.sub1.mod1.main()`(例如,从setup.py中定义的setuptools' entry_points生成)。 - jfs
使用setuptools和entry points会得到加分 - 这是一种适当的设置脚本的方式,可以从外部以明确定义的位置运行,而不是无限制地修改PYTHONPATH。 - RecencyEffect
在PEPs上没有找到“run”的定义。对我来说,它看起来不像是“running”是最好的定义(对于蚂蚁模式),因为最终的“解释”将链接依赖项而不是立即执行它。参考1参考2 - user7075574

48

这个问题已经完全解决:

  • app/
    • main.py
  • settings/
    • local_setings.py

在 app/main.py 中导入 settings/local_setting.py:

main.py:

import sys
sys.path.insert(0, "../settings")


try:
    from local_settings import *
except ImportError:
    print('No Import')

4
谢谢!所有人都要求我以不同的方式运行我的脚本,而不是告诉我如何在脚本中解决它。但我不得不改变代码来使用sys.path.insert(0, "../settings"),然后from local_settings import * - Vit Bernatik
你能解释一下你的答案吗?比如说,为什么它能工作?它是怎么工作的?要点是什么?解释对于一个好的答案来说非常重要。(但请不要包含"编辑:"、"更新:"或类似的内容——答案应该看起来像是今天写的) - Peter Mortensen
@PeterMortensen 这个方法有效是因为它是一个技巧。Python使用path来查找模块。默认情况下,脚本所在的目录会被加入到路径中。这样强制将上一级目录中名为settings的目录插入到路径中。就像所有的技巧一样,这种方法在很多情况下都能起作用,但并不是Python中导入模块的预期方式。其他方法更受推荐。 - CervEd

29

通过示例解释nosklo的答案

注意:所有__init__.py文件都是空的。

main.py
app/ ->
    __init__.py
    package_a/ ->
       __init__.py
       fun_a.py
    package_b/ ->
       __init__.py
       fun_b.py

app/package_a/fun_a.py

def print_a():
    print 'This is a function in dir package_a'

app/package_b/fun_b.py

from app.package_a.fun_a import print_a
def print_b():
    print 'This is a function in dir package_b'
    print 'going to call a function in dir package_a'
    print '-'*30
    print_a()

main.py

from app.package_b import fun_b
fun_b.print_b()

如果你运行 python main.py,它会返回:
This is a function in dir package_b
going to call a function in dir package_a
------------------------------
This is a function in dir package_a
  • main.py文件执行以下操作:from app.package_b import fun_b
  • fun_b.py文件执行以下操作:from app.package_a.fun_a import print_a

因此,位于package_b文件夹中的文件使用了位于package_a文件夹中的文件,这正是您想要的。对吗?


这并没有回答问题。问题是如何进行相对导入;这只展示了绝对导入。 - undefined

24

使用:

def import_path(fullpath):
    """ 
    Import a file with full path specification. Allows one to
    import from anywhere, something __import__ does not do. 
    """
    path, filename = os.path.split(fullpath)
    filename, ext = os.path.splitext(filename)
    sys.path.append(path)
    module = __import__(filename)
    reload(module) # Might be out of date
    del sys.path[-1]
    return module

我正在使用这个片段从路径中导入模块。

2
我正在使用这个片段,结合imp模块(如此处所述[1]),取得了很好的效果。 [1]: https://dev59.com/InNA5IYBdhLWcg3wF5uO#1096247 - Xiong Chiamiov
8
可能需要将 sys.path.append(path) 替换为 sys.path.insert(0, path),并将 sys.path[-1] 替换为 sys.path[0]。否则,如果搜索路径中已经存在同名模块,则该函数将导入错误的模块。例如,如果当前目录中有 "some.py",那么 import_path("/imports/some.py") 将导入错误的文件。 - Alex Che
我同意!有时其他相关的导入会优先。使用sys.path.insert。 - iElectric
如何复制从 x 导入 y (或 *) 的行为? - levesque
2
请明确说明完整使用此脚本以解决 OP 的问题。 - mrgloom

13

很不幸,这是一种 sys.path 的hack方法,但它非常有效。

我遇到了另一个层面的问题:我已经有了一个指定名称的模块,但它是错误的模块。

我想要做的是以下内容(我正在使用的模块是 module3):

mymodule\
   __init__.py
   mymodule1\
      __init__.py
      mymodule1_1
   mymodule2\
      __init__.py
      mymodule2_1


import mymodule.mymodule1.mymodule1_1  

请注意,我已经安装了mymodule,但在我的安装包中找不到“mymodule1”

因为它试图从我安装的模块中进行导入,所以我会收到ImportError错误。

我尝试过sys.path.append,但那并没有起作用。能够起作用的是sys.path.insert

if __name__ == '__main__':
    sys.path.insert(0, '../..')

有点取巧,但是我把所有东西都搞定了! 需要记住的是,如果你想要你的决策<强制覆盖其他路径>,那么你需要使用sys.path.insert(0, pathname)来使其生效!这对我来说是一个非常令人沮丧的难点,很多人说要使用 "append" 函数到sys.path中,但是如果你已经定义了一个模块,那么它就不起作用了(我认为这是一种非常奇怪的行为)


sys.path.append('../') 对我来说很有效(Python 3.5.2) - Nister
我认为这很好,因为它将黑客定位到可执行文件,并不影响可能依赖于您的软件包的其他模块。 - Tom Russell

10

这里只是为了自己参考而放置。我知道这不是好的Python代码,但我需要一个脚本来完成我的项目,并且我想把这个脚本放在scripts目录下。

import os.path
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

8

这并没有回答问题。问题是如何正确使用相对导入,而不是“请让我能够导入文件”。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接