Python 3中的相对导入无法工作

54

我有如下目录:

mydirectory
├── __init__.py
├── file1.py 
└── file2.py

我在file1.py中定义了一个函数f。

如果我在file2.py中执行以下操作:

from .file1 import f

我遇到了以下错误:

SystemError: 未加载父模块“”,无法执行相对导入

为什么会出现这个错误?如何解决?


你是直接运行 file2.py 吗? - Blender
是的,我正在使用命令行执行以下操作:python3 file2.py - John Smith Optional
20
如果一个Python模块是某个包的一部分,不应该将其作为主要模块来运行。如果您要分发您的库,那么包将会进入site-packages,但脚本应该放在/usr/bin或类似位置(因此需要使用绝对导入)。应该清楚地区分编写以执行和编写作为库一部分的Python模块之间的区别。 - Bakuriu
1
@Bakuriu 您的评论应该是一个答案。 - Kevin
1
@kevin 谢谢。我决定稍微扩展一下并将其作为答案添加。 - Bakuriu
4个回答

45

将包内的模块作为可执行文件启动是一种不良实践。

在开发时,你可以构建一个库,它旨在被其他程序导入,因此不应该允许直接执行其子模块,或者你可以构建一个可执行文件,在这种情况下,没有理由将其作为包的一部分。

这就是为什么在setup.py中要区分包和脚本。包将放置在site-packages下,而脚本将安装在/usr/bin(或类似位置,具体取决于操作系统)。

我的建议是使用以下布局:

/
├── mydirectory
|    ├── __init__.py
|    ├── file1.py 
└── file2.py

file2.py 导入 file1.py 时,它像任何想要使用库 mydirectory 的其他代码一样进行了绝对导入:

from mydirectory.file1 import f
当您为项目编写setup.py脚本时,只需将mydirectory列为一个包,将file2.py列为脚本,一切都会正常工作。无需操作sys.path。 如果您因某种原因确实想要运行包的子模块,则正确的方法是使用-m开关:
python -m mydirectory.file1

这样加载整个包并将模块作为脚本执行,使得相对导入可以成功。

我个人会避免这样做。另外很多人甚至不知道可以这样做,并且最终会遇到与你一样的错误,并认为包已损坏。


关于当前被接受的答案,它说你应该只使用一个隐式的相对导入from file1 import f,因为它可以工作,因为它们在同一个目录中:

这是错误的

  • 在禁止使用隐式相对导入的Python 3中,它将不能正常工作,并且如果您恰好安装了file1模块,它肯定会出错(因为它将被导入而不是您的模块!)。
  • 即使它能工作,file1也不会被视为mydirectory包的一部分。这可能很重要。

    例如,如果file1使用pickle,则包的名称对于正确加载/卸载数据非常重要。


1
如果您在同一目录中有一个单元测试文件,并且想要运行该单元测试文件,这是否也是一种不良实践? - gsanta
@gsanta 为什么你要把单元测试放在那里?典型的项目结构应该有一个名为 testtests 的目录,其中包含所有的测试文件,而实际的源代码则在另一个目录中。 - Bakuriu
我来自“JavaScript世界”,在那里有一个越来越流行的趋势,即将单元测试文件放在要测试的文件旁边,我认为这是一个很好的做法。但在Python编程中可能不太流行。(有时候当我想快速验证它是否工作时,我只想运行那个测试文件而不是所有的单元测试文件) - gsanta
1
@gsanta 看起来 JavaScript 缺乏适当的测试发现工具。例如,在 Python 中使用 Nose,自动发现测试非常简单,而且您只需使用测试名称作为参数调用它,它就会仅执行这些测试...如果您只从 IDE 工作,它们通常提供内置测试器,可以做同样的事情。基本上,如果您手动完成所有操作,则只需要稍微方便一些。 - Bakuriu
不用了,JS 已经有了这个功能。我也是来自 JS 世界,并发现这种所谓的“坏习惯”有点奇怪。 - Tucker Connelly

24

当启动Python源文件时,禁止使用相对导入方式导入当前包中的另一个文件。

文档中指出:

注意,相对导入是基于当前模块名称的。由于主模块的名称始终为“__main__”,因此用作Python应用程序主模块的模块必须始终使用绝对导入。

因此,如@mrKelley所说,您需要在这种情况下使用绝对导入。


1
谢谢!这个简单明了的答案终于让我明白为什么在命令行运行时相对导入没有像我想象的那样工作了! - Xavier

23

由于 file1file2 在同一个目录中,因此您甚至不需要有一个 __init__.py 文件。 如果您将要扩展规模,则保留它。

要在同一目录中的文件中导入某些内容,只需像这样操作:

from file1 import f

即,您不需要进行相对路径 .file1,因为它们在同一个目录中。

如果您的主函数、脚本或整个应用程序的运行位置在另一个目录中,则您必须将所有内容相对于正在执行的位置进行处理。


6
假设PYTHONPATH中没有其他名为file1的文件。 - Jens

0
myproject/

mypackage
├── __init__.py
├── file1.py
├── file2.py 
└── file3.py

mymainscript.py

从一个文件导入到另一个文件的示例

#file1.py
from myproject import file2
from myproject.file3 import MyClass

在主脚本中导入example包

#mymainscript.py
import mypackage

https://docs.python.org/3/tutorial/modules.html#packages

https://docs.python.org/3/reference/import.html#regular-packages

https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

https://docs.python.org/3/glossary.html#term-import-path

变量 sys.path 是一个字符串列表,它确定解释器在模块搜索路径中的查找顺序。它初始化为一个默认路径,该路径取自环境变量 PYTHONPATH,如果未设置 PYTHONPATH,则取自内置默认路径。您可以使用标准列表操作来修改它:
import sys
sys.path.append('/ufs/guido/lib/python')
sys.path.insert(0, '/ufs/guido/myhaxxlib/python')

将其插入到开头的好处在于,确保在命名冲突的情况下,在其他路径(甚至是内置路径)之前搜索该路径。

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