概述
Python中的相對導(dǎo)入(Relative Import)有時是很神秘和晦澀的。有時會遇到ValueError: Attempted relative import beyond top-level package這樣的錯誤琐鲁。讓我們看下如何修正這個問題卫旱。
重現(xiàn)
假設(shè)有如下項目結(jié)構(gòu)
test_py
│ main.py
│ __init__.py
│
├─tests
│ mytest.py
│ __init__.py
│
└─tools
doo.py
__init__.py
tools/__init__.py
內(nèi)容如下所示
def tool_init():
return 'this is tool_init'
現(xiàn)在需要在tests/mytest.py
中調(diào)用tools/__init__.py
中定義的函數(shù)人灼,很可能會這么寫:
from ..tools import tool_init
print(tool_init())
執(zhí)行結(jié)果如下:
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
這個錯誤字面意思是:相對導(dǎo)入超過了top-level围段。導(dǎo)致這個問題原因是錯誤地將相對導(dǎo)入理解成相對路徑導(dǎo)入。
官方解釋
在PEP 328 -- Imports: Multi-Line and Absolute/Relative中可以找到python解釋器是怎樣解釋相對導(dǎo)入的:
相對導(dǎo)入使用模塊的name屬性來確定模塊在包中的層次位置投放。如果模塊的名稱不包含任何包的信息(name被設(shè)置為'main')奈泪。那么相對導(dǎo)入將把模塊解析成top-level模塊,而不關(guān)心模塊在實際文件系統(tǒng)中的位置灸芳。
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
這種直接啟動的方式執(zhí)行代碼時,mytest.py
被視為top-level模塊烙样。而mytest.py
中的from ..tools import tool_init
就超過了top-level(beyond top-level package)冯遂,所以出現(xiàn)ValueError: attempted relative import beyond top-level package
的錯誤。
探索
修改python tests/mytest.py
代碼谒获,如下所示:
from tools import tool_init
print(tool_init())
# 執(zhí)行結(jié)果如下:
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'
上面的錯誤表示蛤肌,在當(dāng)前的路徑中壁却,找不到tools模塊。修改tests/mytest.py
代碼裸准,查看當(dāng)前路徑:
import sys
for p in sys.path:
print(p)
# 執(zhí)行結(jié)果如下:
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
在上面結(jié)果所示的路徑中展东,是找不到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())
執(zhí)行時砸王,進(jìn)入tests目錄,再執(zhí)行mytest.py文件峦阁,結(jié)果如下:
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
參數(shù)处硬,該選項使用python模塊命名空間定位模塊,以作為i腳本的方式執(zhí)行(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())
執(zhí)行時荷辕,再頂層目錄下執(zhí)行,且執(zhí)行的文件寫成tests.mytest
的形式件豌。執(zhí)行結(jié)果如下:
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
總結(jié)
方式一和方式二疮方,對應(yīng)的是兩種啟動方式:
- 方式一,直接啟動茧彤,python xxx.py骡显。將xxx.py所在的路徑放入
sys.path
中 - 方式二,模塊啟動曾掂,python -m xxx惫谤。將啟動python命令的路徑放入
sys.path
中
兩中啟動方式的主要區(qū)別就在于,將不同的路徑放入sys.path
中珠洗。因此溜歪,當(dāng)啟動一個py文件時,需要考慮许蓖,該py文件所引用的包在sys.path
中的路徑中是否能找到蝴猪。