如何修复 "Attempted relative import in non-package" 错误,即使已经有 __init__.py 文件存在。

834

我正在尝试遵循PEP 328,使用以下目录结构:

pkg/
  __init__.py
  components/
    core.py
    __init__.py
  tests/
    core_test.py
    __init__.py

core_test.py中,我有以下导入语句

from ..components.core import GameLoopEvents

然而,当我运行时,我会得到以下错误:

tests$ python core_test.py 
Traceback (most recent call last):
  File "core_test.py", line 3, in <module>
    from ..components.core import GameLoopEvents
ValueError: Attempted relative import in non-package

我查找了一下"即使有__init__.py也无法工作的相对路径"和"从相对路径导入模块",但它们并没有帮助到我。

这里还有什么我需要注意的吗?


22
我也曾困惑于各种不同的unittest项目结构方式,因此我编写了这个相当全面的示例项目,涵盖模块的深度嵌套、相对和绝对导入(何时起作用与不起作用)、包内相对和绝对引用,以及单个、双重和包级别类的导入。这帮助我很快地清楚了所有问题! - cod3monk3y
1
我无法让你的测试工作。每次运行它们时,都会出现“没有名为myimports.foo的模块”的错误。 - Blairg23
@Blairg23 我猜测你想要执行的命令是 cdPyImports 目录下,然后运行 python -m unittest tests.test_abs - duozmo
7
我同意Gene的观点。我希望有一种更有用的导入过程调试机制。在我的情况下,我有两个文件在同一个目录中。我尝试将一个文件导入另一个文件。如果在该目录中有一个__init__.py文件,则会出现ValueError:尝试在非包中进行相对导入的错误。如果删除__init__.py文件,则会出现模块名为'NAME'的错误。 - Jeff Silverman
请看这个链接:https://dev59.com/5mYq5IYBdhLWcg3w30Tc#14132912 - Sabito stands with Ukraine
显示剩余2条评论
23个回答

684
为了进一步解释Ignacio Vazquez-Abrams的答案:
Python导入机制是根据当前文件的__name__确定的。当您直接执行文件时,它没有其通常的名称,而是使用"__main__"作为其名称。所以相对导入无法工作。
可以像Ignacio建议的那样使用-m选项来执行它。如果您的某个包的一部分旨在作为脚本运行,则还可以使用__package__属性告诉该文件它在包层次结构中应具有的名称。
详情请参见http://www.python.org/dev/peps/pep-0366/

65
我花了一些时间才意识到,无法在tests子目录中运行python -m core_test命令,必须在父级目录中运行,或者将父级目录添加到路径中。 - Aram Kocharyan
3
并不完全如此。您可以使用__package__确保可执行脚本文件可以相对导入同一包中的其他模块。没有办法从“整个系统”中进行相对导入。我甚至不确定为什么您想这样做。 - BrenBarn
2
我的意思是,如果__package__符号设置为“parent.child”,那么您将能够导入“parent.other_child”。也许我没有表达清楚。 - Danny Staple
6
如何运作在链接的文档中有描述。如果你在包pack.subpack中有一个脚本script.py,将它的__package__设置为pack.subpack,则可以使用from ..module import something来从pack.module中导入something。请注意,正如文档所述,您仍然必须在系统路径上拥有顶级包。对于已导入的模块,这已经是工作方式。__package__唯一的作用是让你也可以用这种方式来处理直接执行的脚本。 - BrenBarn
3
在我直接执行的脚本中,我使用了 __package__,但不幸的是,我遇到了以下错误:"无法加载父模块 'xxx',无法执行相对导入"。 - mononoke
显示剩余6条评论

474

是的,你没有将它用作包。

python -m pkg.tests.core_test

55
注意:这里结尾没有'.py'! - mindthief
538
我不是点赞者中的任何一个,但我觉得考虑到这个问题和答案的受欢迎程度,它需要更多详细信息。指出一些细节,比如应该从哪个目录执行上面的 shell 命令,需要一路创建 __init__.py 文件,以及需要修改 __package__ 的技巧(由 BrenBarn 在下面描述),以允许在可执行脚本中进行导入(例如,在 Unix shell 中使用 shebang 并执行 ./my_script.py 时)。所有这些都将是有用的。这个问题对我来说相当棘手,我很难找到简明易懂的文档或解决方法。 - Mark Amery
23
注意:在使用CLI调用此行时需要在pkg目录之外。然后,它应该按预期工作。如果你在pkg目录内并调用python -m tests.core_test,它将无法正常工作。至少对我来说是这样的。 - Blairg23
106
当然,你能解释一下你的回答中正在发生什么吗? - Charlie Parker
21
@MarkAmery 几乎失去理智,试图理解这一切是如何工作的,项目中存在子目录和带有 __init__.py 文件的 py 文件的相对导入,但仍然不断收到 ValueError: Attempted relative import in non-package 错误。我愿意支付相当高的费用,请有人最终用简单易懂的英语解释所有这些是如何工作的。 - AdjunctProfessorFalcon
显示剩余15条评论

227

这取决于您希望如何启动您的脚本。

如果您想以传统方式从命令行启动您的单元测试,即:

python tests/core_test.py

那么,在这种情况下,由于'components''tests'是兄弟文件夹,您可以使用sys.path模块的insertappend方法导入相对模块。 类似如下:

import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
from components.core import GameLoopEvents
否则,您可以使用“-m”参数启动您的脚本(请注意,在此情况下,我们正在谈论一个包,因此您不必给出“.py”扩展名),即: 使用“-m”参数启动您的脚本
python -m pkg.tests.core_test
在这种情况下,您可以像以前一样使用相对导入:
from ..components.core import GameLoopEvents

你最终可以将这两种方法结合起来,使得你的脚本无论以何种方式被调用都能正常工作。例如:

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

3
如果我正在尝试使用pdb进行调试,我该怎么办?因为你需要使用python -m pdb myscript.py来启动调试会话。 - danny
1
@dannynjust -- 这是一个好问题,因为你不能有两个主模块。通常在调试时,我更喜欢手动进入调试器,从我想要开始调试的第一个点开始。您可以通过将 import pdb; pdb.set_trace() 插入代码(内联)来实现这一点。 - mgilson
3
使用insert代替append是否更好?也就是说,sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - SparkAndShine
2
使用insert更适合相对导入语义,其中本地包名称优先于已安装的包。特别是对于测试,通常希望测试本地版本,而不是已安装的版本(除非您的测试基础架构安装了要测试的代码,在这种情况下,相对导入是不需要的,您将不会遇到此问题)。 - Alex Dupuy
2
你还应该提到,当你以模块方式运行时,不能在包含 core_test 的目录中运行(那会太容易了)。 - Joseph Garvin

213

如果将当前目录添加到sys.path中,则可以直接使用import components.core

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

38
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))这行代码的作用是将上一级目录添加到Python解释器搜索模块的路径列表中。 - ajay
32
os 模块导入 sys,看起来像是作弊 :) - flying sheep
3
可能认为这样更好,因为它稍微清楚地显示了正在添加到 sys.path 的内容——当前文件所在目录的父目录。 - martineau
8
同意,我只会使用常规的 import sys, os.path as path - martineau
11
供参考:若想在 ipython notebook 中使用此方法,我根据这个答案进行了修改:import os; os.sys.path.append(os.path.dirname(os.path.abspath('.')))。然后直接使用import components.core即可,这样可以从笔记本的父目录中导入所需内容。 - Racing Tadpole
显示剩余7条评论

34

在 core_test.py 文件中,进行以下操作:

import sys
sys.path.append('../components')
from core import GameLoopEvents

13
问题出在你的测试方法上。 你尝试了python core_test.py,然后会得到这个错误:

ValueError: Attempted relative import in non-package

原因是你从非包源代码处测试你的打包。 所以,请从包源代码处测试你的模块。 如果这是你的项目结构,
pkg/
  __init__.py
  components/
    core.py
    __init__.py
  tests/
    core_test.py
    __init__.py

cd pkg

python -m tests.core_test # dont use .py

或者来自 pkg/ 之外

python -m pkg.tests.core_test

如果你想从同一目录下的文件夹中导入文件,只需使用单个 . 。 每向后退一步,就再添加一个。

hi/
  hello.py
how.py

how.py 中。
from .hi import hello

如果你想从 hello.py 导入模块:

from .. import how

在示例 from .. import how 中,如何从 'how' 文件中导入特定的类/方法。当我执行 from ..how import foo 的等效操作时,会出现“尝试超出顶级包的相对导入”的错误提示。 - James Hulse
@JamesHulse from .. import how 能够工作,但第二个语句为什么不行呢?如果包含 hi 文件夹的文件夹中没有 _ _ init _ _.py 文件,我本来会认为这两个语句都不能工作。换句话说,如果只有 hi 文件夹中有一个 init 文件,那么 hi 文件夹就是顶级包,你无法超越它。 - Isi

11

如果您的使用情况是运行测试,而且看起来确实是这样,那么您可以执行以下操作。不要像这样运行您的测试脚本:python core_test.py,而是使用诸如pytest之类的测试框架。然后在命令行上输入:

$$ py.test

运行这段代码将测试你的目录。这解决了 @BrenBarn 指出的__name____main__的问题。接下来,在您的测试目录中放置一个空的__init__.py文件,这将使测试目录成为您的包的一部分。然后,您就可以执行以下操作:

from ..components.core import GameLoopEvents

然而,如果您将测试脚本作为主程序运行,则会再次失败。因此,只需使用测试运行器。也许这对其他测试运行器(如nosetests)也适用,但我还没有检查过。希望这可以帮助到您。


9

我的快速解决方案是将目录添加到路径中:

import sys
sys.path.insert(0, '../components/')

8
你的方法并不适用于所有情况,因为'../'部分是从运行你的脚本(core_test.py)所在的目录来解析的。使用你的方法,你必须在运行core_test.py之前切换到'tests'目录。 - xyman

7

由于您已将所有内容标记为模块,因此如果您将其作为Python模块启动,则无需使用相对引用。

与其使用

from ..components.core import GameLoopEvents

简单地

from pkg.components.core import GameLoopEvents

当你从包的父级运行时,请使用以下命令:

python -m pkg.tests.core_test

不错!我在使用这种导入方式测试我的包时遇到了麻烦,因为Python会选择安装的旧版本而不是我正在测试的新版本。尤其是在运行python pkg/tests/core_test.py或类似命令时。但是使用-m选项解决了这个问题,现在似乎更喜欢本地副本,我想这是因为它现在被视为一个模块。这让我想知道相对导入何时是一个好主意... - Ben Farmer

4

正如Paolo所说,我们有两种调用方法:

  1. python -m tests.core_test
  2. python tests/core_test.py

它们之间的一个区别是 sys.path[0] 字符串。由于解释器在进行导入时会搜索 sys.path, 我们可以使用 tests/core_test.py

if __name__ == '__main__':
    import sys
    from pathlib import Path
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
    from components import core
    <other stuff>

此外,我们还可以使用其他方法运行core_test.py

cd tests
python core_test.py
python -m core_test
...

注意,仅在Python 3.6上进行了测试。


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