似乎这里已经有很多关于 python 3 中的相对导入的问题,但是在经历了许多问题之后,我仍然没有找到我的问题的答案。所以这就是问题所在。
我有一个如下所示的包
package/
__init__.py
A/
__init__.py
foo.py
test_A/
__init__.py
test.py
我在 test.py 中有一行:
from ..A import foo
现在,我在 package
的文件夹中,我运行
python -m test_A.test
我收到消息
"ValueError: attempted relative import beyond top-level package"
但如果我在 package
的父文件夹中,例如,我运行:
cd ..
python -m package.test_A.test
一切都很好。
现在我的问题是:当我在 package
的文件夹中,并且我将 test_A 子包中的模块作为 test_A.test
运行时,根据我的理解,..A
只会上升一个级别,仍然在 package
文件夹中,为什么它会给出消息说 beyond top-level package
。导致此错误消息的确切原因是什么?
编辑:在其他问题中有更好/更连贯的答案:
兄弟包导入
十亿次相对进口
为什么不起作用?这是因为 python 不记录从哪里加载包。因此,当您执行 python -m test_A.test
时,它基本上只是丢弃了 test_A.test
实际存储在 package
中的知识(即 package
不被视为包)。尝试 from ..A import foo
正在尝试访问它不再拥有的信息(即已加载位置的同级目录)。它在概念上类似于在 math
中的文件中允许 from ..os import path
。这会很糟糕,因为您希望包是不同的。如果他们需要使用另一个包中的某些东西,那么他们应该使用 from os import path
全局引用它们,并让 python 找出 $PATH
和 $PYTHONPATH
的位置。
当您使用 python -m package.test_A.test
时,使用 from ..A import foo
就可以解决问题,因为它会跟踪 package
中的内容,并且您只是访问已加载位置的子目录。
为什么python不认为当前工作目录是一个包?没有线索,但天哪,它会很有用。
import sys
sys.path.append("..") # Adds higher directory to python modules path.
尝试这个。为我工作。
假设:
如果您在 package
目录中,则 A
和 test_A
是单独的包。
结论:
..A
只允许在包中导入。
进一步说明:
如果您想强制将包放置在位于 sys.path
上的任何路径上,则使相对导入仅在包中可用是有用的。
编辑:
只有我一个人认为这很疯狂!?为什么当前的工作目录不被认为是一个包? - 多猎人
当前工作目录通常位于 sys.path 中。因此,那里的所有文件都是可导入的。这是自 Python 2 以来包尚不存在时的行为。将运行目录设为包将允许将模块导入为“import .A”和“import A”,这将是两个不同的模块。也许这是一个需要考虑的不一致之处。
python -m package.test_A.test
的行为似乎符合我们的要求,我的论点是这应该是默认值。那么,你能给我举一个这种不一致的例子吗?
#include
会非常有用!
这些解决方案在 3.6 中都不适合我,其文件夹结构如下:
package1/
subpackage1/
module1.py
package2/
subpackage2/
module2.py
我的目标是从模块 1 导入模块 2。最终对我有用的是,奇怪的是:
import sys
sys.path.append(".")
请注意单点,而不是到目前为止提到的两点解决方案。
编辑:以下内容帮助我澄清了这一点:
import os
print (os.getcwd())
就我而言,工作目录(出乎意料地)是项目的根目录。
sys.path.append(".")
有效,因为您在父目录中调用它,请注意 .
始终代表您在其中运行 python 命令的目录。
这在 Python 中非常棘手。
我将首先评论您为什么会遇到这个问题,然后我会提到两种可能的解决方案。
这是怎么回事?
您必须考虑 Python documentation 中的这一段:
请注意,相对导入基于当前模块的名称。由于主模块的名称始终为“main”,因此用作 Python 应用程序的主模块的模块必须始终使用绝对导入。
还有来自 PEP 328 的以下内容:
相对导入使用模块的名称属性来确定该模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,它被设置为'main'),那么无论模块实际位于文件系统上的哪个位置,都将相对导入解析为就好像该模块是顶级模块一样。
相对导入从文件名(__name__
属性)开始,它可以采用两个值:
它是文件名,前面是文件夹结构,用点分隔。例如: package.test_A.test 这里 Python 知道父目录:在 test 之前是 test_A,然后是 package。因此,您可以使用点表示法进行相对导入。
# package.test_A/test.py
from ..A import foo
然后,您可以在调用 test.py
的根目录中拥有类似的根文件:
# root.py
from package.test_A import test
当你直接运行模块(test.py)时,它成为程序的入口点,所以__name__ == __main__。文件名没有指示目录结构,所以 Python 不知道如何在目录中上去。对于 Python,test.py 成为顶级脚本,它上面没有任何东西。这就是为什么你不能使用相对导入。
可能的解决方案
A)解决这个问题的一种方法是有一个调用模块/包的根文件(在根目录中),如下所示:
https://i.stack.imgur.com/0Vyhc.jpg
root.py 导入 test.py。 (入口点,__name__ == __main__)。
test.py(相对)导入 foo.py。
foo.py 表示模块已被导入。
输出是:
package.A.foo has been imported
Module's name is: package.test_A.test
B)如果您想将代码作为模块而不是作为顶级脚本执行,您可以从命令行尝试:
python -m package.test_A.test
欢迎任何建议。
您还应该检查: Relative imports for the billionth time ,特别是 BrenBarn 的答案。
from package.A import foo
我觉得比
import sys
sys.path.append("..")
sys.path.append("..")
。在 python 3.6 上测试
ImportError
。
正如最受欢迎的答案所暗示的那样,基本上是因为您的 PYTHONPATH
或 sys.path
包含 .
但不包含您的包裹路径。并且相对导入是相对于您当前的工作目录,而不是发生导入的文件;奇怪。
您可以通过首先将相对导入更改为绝对导入,然后以以下方式启动它来解决此问题:
PYTHONPATH=/path/to/package python -m test_A.test
或以这种方式调用时强制使用 python 路径,因为:
使用 python -m test_A.test
,您正在使用 __name__ == '__main__'
和 __file__ == '/absolute/path/to/test_A/test.py'
执行 test_A/test.py
这意味着在 test.py
中,您可以在主要情况下使用绝对 import
半保护,并且还可以进行一些一次性 Python 路径操作:
from os import path
…
def main():
…
if __name__ == '__main__':
import sys
sys.path.append(path.join(path.dirname(__file__), '..'))
from A import foo
exit(main())
top-level package
为 test_A
,并且相对导入尝试遍历父 package
目录,从文件开始,而不是当前工作目录。
编辑:2020-05-08:似乎我引用的网站不再由撰写建议的人控制,所以我删除了该网站的链接。谢谢你让我知道baxx。
如果有人在已经提供了很好的答案后仍然有点挣扎,我在一个不再可用的网站上找到了建议。
我提到的网站的基本报价:
“同样可以通过这种方式以编程方式指定:import sys sys.path.append('..') 当然上面的代码必须写在另一个 import 语句之前。
很明显,它必须是这样的,事后考虑。我试图在我的测试中使用 sys.path.append('..') ,但遇到了 OP 发布的问题。通过在我的其他导入之前添加导入和 sys.path 定义,我能够解决问题。
这实际上比其他答案要简单得多。
TL;DR:直接导入 A
,而不是尝试相对导入。
当前工作目录不是包,除非您从其他文件夹导入文件夹 package
。因此,如果您打算将其导入其他应用程序,您的包的行为将正常工作。什么不工作是测试...
无需更改目录结构中的任何内容,只需更改 test.py
导入 foo.py
的方式。
from A import foo
现在从 package
目录运行 python -m test_A.test
将在没有 ImportError
的情况下运行。
为什么这行得通?
您当前的工作目录不是一个包,但它被添加到了路径中。因此,您可以直接导入文件夹 A
及其内容。这与您可以导入已安装的任何其他软件包的原因相同......它们都包含在您的路径中。
package/
开始的特定情况(即仅在 intended 包根内部,而不是仅在外部)。
如果您在上层文件夹中有 __init__.py
,则可以在该初始化文件中将导入初始化为 import file/path as alias
。然后你可以在较低的脚本上使用它:
import alias
__init__.py
与问题无关。
就我而言,我不得不更改为:解决方案 1(更好,取决于当前的 py 文件路径。易于部署)使用 pathlib.Path.parents make code cleaner
import sys
import os
import pathlib
target_path = pathlib.Path(os.path.abspath(__file__)).parents[3]
sys.path.append(target_path)
from utils import MultiFileAllowed
解决方案 2
import sys
import os
sys.path.append(os.getcwd())
from utils import MultiFileAllowed
以我的拙见,我是这样理解这个问题的:
[案例 1] 当你开始一个绝对导入时
python -m test_A.test
或者
import test_A.test
或者
from test_A import test
您实际上将 import-anchor 设置为 test_A
,换句话说,顶级包是 test_A
。因此,当我们让 test.py 执行 from ..A import xxx
时,您正在逃离锚点,而 Python 不允许这样做。
[案例 2] 当你这样做时
python -m package.test_A.test
或者
from package.test_A import test
您的锚点变为 package
,因此 package/test_A/test.py
执行 from ..A import xxx
不会逃脱锚点(仍在 package
文件夹中),Python 很乐意接受这一点。
简而言之:
Absolute-import 改变当前锚点(=重新定义什么是顶级包);
相对导入不会更改锚点,但仅限于它。
此外,我们可以使用 full-qualified module name(FQMN) 来检查这个问题。
在每种情况下检查 FQMN:
[CASE2] test.__name__ = package.test_A.test
[CASE1] test.__name__ = test_A.test
因此,对于 CASE2,from .. import xxx
将产生一个 FQMN=package.xxx
的新模块,这是可以接受的。
而对于 CASE1,from .. import xxx
中的 ..
将跳出 test_A
的 起始节点(锚点),这是 Python 不允许的。
[2022-07-19] 我认为这种“相对导入”限制是一个非常丑陋的设计,完全违背了 Python 的座右铭“简单胜于复杂”。
在 python 2.x 中不确定,但在 python 3.6 中,假设您尝试运行整个套件,您只需要使用 -t
-t, --top-level-directory directory 项目的顶级目录(默认为起始目录)
所以,在这样的结构上
project_root
|
|----- my_module
| \
| \_____ my_class.py
|
\ tests
\___ test_my_func.py
例如可以使用:
python3 unittest discover -s /full_path/project_root/tests -t /full_path/project_root/
并且仍然导入没有主要戏剧的my_module.my_class
。
有
package/
__init__.py
A/
__init__.py
foo.py
test_A/
__init__.py
test.py
在 A/__init__.py
导入 foo
:
from .foo import foo
从 test_A/
导入 A/
时
import sys, os
sys.path.append(os.path.abspath('../A'))
# then import foo
import foo
只需删除 test.py 中的 ..
对我来说 pytest 就可以很好地使用该
示例:
from A import foo
-m
标志并从上面的目录运行。sys.path
hack,而是使用 setuptools,我认为这更有趣。-m
选项指定指定包的包根路径。