概述
Python中的相对导入(Relative Import)有时是很神秘和晦涩的。有时会遇到ValueError: Attempted relative import beyond top-level package这样的错误。让我们看下如何修正这个问题。
重现
假设有如下项目结构
test_py
│ main.py
│ __init__.py
│
├─tests
│ mytest.py
│ __init__.py
│
└─tools
doo.py
__init__.py
tools/__init__.py
内容如下所示
def tool_init():
return 'this is tool_init'
现在需要在tests/mytest.py
中调用tools/__init__.py
中定义的函数,很可能会这么写:
from ..tools import tool_init
print(tool_init())
执行结果如下:
D:\Documents\JavaSpace\test_py>python tests/mytest.py
Traceback (most recent call last):
File "tests/mytest.py", line 1, in <module>
from ..tools import tool_init
ValueError: attempted relative import beyond top-level package
这个错误字面意思是:相对导入超过了top-level。导致这个问题原因是错误地将相对导入理解成相对路径导入。
官方解释
在PEP 328 -- Imports: Multi-Line and Absolute/Relative中可以找到python解释器是怎样解释相对导入的:
相对导入使用模块的name属性来确定模块在包中的层次位置。如果模块的名称不包含任何包的信息(name被设置为'main')。那么相对导入将把模块解析成top-level模块,而不关心模块在实际文件系统中的位置。
Relative imports use a module's name attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to 'main') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
因此,以python tests/mytest.py
这种直接启动的方式执行代码时,mytest.py
被视为top-level模块。而mytest.py
中的from ..tools import tool_init
就超过了top-level(beyond top-level package),所以出现ValueError: attempted relative import beyond top-level package
的错误。
探索
修改python tests/mytest.py
代码,如下所示:
from tools import tool_init
print(tool_init())
# 执行结果如下:
D:\Documents\JavaSpace\test_py>python tests/mytest.py
Traceback (most recent call last):
File "tests/mytest.py", line 1, in <module>
from tools import tool_init
ModuleNotFoundError: No module named 'tools'
上面的错误表示,在当前的路径中,找不到tools模块。修改tests/mytest.py
代码,查看当前路径:
import sys
for p in sys.path:
print(p)
# 执行结果如下:
D:\Documents\JavaSpace\test_py>python tests/mytest.py
D:\Documents\JavaSpace\test_py\tests
D:\Anaconda3\python37.zip
D:\Anaconda3\DLLs
D:\Anaconda3\lib
D:\Anaconda3
D:\Anaconda3\lib\site-packages
D:\Anaconda3\lib\site-packages\win32
D:\Anaconda3\lib\site-packages\win32\lib
D:\Anaconda3\lib\site-packages\Pythonwin
在上面结果所示的路径中,是找不到tools模块的。tools模块在D:\Documents\JavaSpace\test_py
中。解决这个问题有两种方式。
方式一
在代码中增加sys.path.append('..')
代码,增加父级路径。修改 tests/mytest.py
代码如下所示:
import sys
sys.path.append('..')
from tools import tool_init
for p in sys.path:
print(p)
print('*'*20)
print(tool_init())
执行时,进入tests目录,再执行mytest.py文件,结果如下:
D:\Documents\JavaSpace\test_py\tests>python mytest.py
D:\Documents\JavaSpace\test_py\tests
D:\Anaconda3\python37.zip
D:\Anaconda3\DLLs
D:\Anaconda3\lib
D:\Anaconda3
D:\Anaconda3\lib\site-packages
D:\Anaconda3\lib\site-packages\win32
D:\Anaconda3\lib\site-packages\win32\lib
D:\Anaconda3\lib\site-packages\Pythonwin
..
********************
this is tool_init
方式二
使用-m
参数,该选项使用python模块命名空间定位模块,以作为i脚本的方式执行(The python -m option allows modules to be located using the Python module namespace for execution as scripts)。修改tests/mytest.py
代码如下所示:
import sys
from tools import tool_init
for p in sys.path:
print(p)
print('*'*20)
print(tool_init())
执行时,再顶层目录下执行,且执行的文件写成tests.mytest
的形式。执行结果如下:
D:\Documents\JavaSpace\test_py>python -m tests.mytest
D:\Documents\JavaSpace\test_py
D:\Anaconda3\python37.zip
D:\Anaconda3\DLLs
D:\Anaconda3\lib
D:\Anaconda3
D:\Anaconda3\lib\site-packages
D:\Anaconda3\lib\site-packages\win32
D:\Anaconda3\lib\site-packages\win32\lib
D:\Anaconda3\lib\site-packages\Pythonwin
********************
this is tool_init
总结
方式一和方式二,对应的是两种启动方式:
- 方式一,直接启动,python xxx.py。将xxx.py所在的路径放入
sys.path
中 - 方式二,模块启动,python -m xxx。将启动python命令的路径放入
sys.path
中
两中启动方式的主要区别就在于,将不同的路径放入sys.path
中。因此,当启动一个py文件时,需要考虑,该py文件所引用的包在sys.path
中的路径中是否能找到。