三种启动方式实测:-m 换掉 sys.path[0],相对导入在直接运行时反而报错
同一个包,三种启动方式,三种结果
一个包,目录结构是这样:
mpkg/
__init__.py
modx.py
show.py
show.py 里有一句 import modx。
我在本机(Windows 10 + NTFS,Python 3.13.14)把这几种跑法都试了一遍,每种都打印 os.getcwd()、sys.path[0]、__package__,再分别试平铺导入和相对导入。
跑法一,在包目录里直接跑 python show.py:
sys.path[0]是包所在目录__package__是Noneimport modx成功
跑法二,在上一级目录直接跑 python mpkg/show.py:
sys.path[0]还是包所在目录,不是当前工作目录__package__是Noneimport modx成功
跑法三,在上一级目录用模块方式跑 python -m mpkg.show:
sys.path[0]变成了当前工作目录,也就是上一级__package__是'mpkg'import modx报ModuleNotFoundError: No module named 'modx'
同一个文件,同一个包,三种跑法两种能用一种不能。差别就落在 sys.path[0] 这一处。
相对导入不是万能钥匙
常见说法是,包内引用改成相对导入 from . import modx,就跟在哪种方式启动无关了。我实测下来不成立:
- 直接跑(cwd 在包内):
from . import modx报ImportError: attempted relative import with no known parent package - 直接跑(cwd 在上一级):同样报这个错
- 用
-m跑(cwd 在上一级):from . import modx成功
原因在 __package__。相对导入靠的是「当前模块属于哪个包」,而直接运行时 __package__ 是 None,Python 根本不知道当前文件属于哪个包,相对的那个点就没有锚点。
所以相对导入并非换个写法就万事大吉,它只在有包上下文的时候才成立。
一个反直觉的坑:模块方式跑过的模块,再 import 会执行两遍
我写了个 side.py,模块顶层就一句打印。跑 python -m mpkg.side,模块体执行一次,__name__ 是 __main__。
有意思的是跑完之后 sys.modules 里没有 mpkg.side 这一项,只有 mpkg。因为用 -m 启动时,模块是以 __main__ 的身份执行的,并没有按 mpkg.side 这个名字注册进 sys.modules。
这意味着随后再写一句 import mpkg.side,模块体会再执行一遍,这次 __name__ 是 mpkg.side。
两份模块对象互相独立,一份挂在 __main__ 下,一份挂在 mpkg.side 下。模块顶层做初始化、建连接、注册单例的时候,这两遍都要算进去。
模块方式要在包的上一级跑
在包目录内部执行 python -m mpkg.show,报的是另一种错:
ModuleNotFoundError: No module named 'mpkg'
跟上面那个「找不到 modx」完全不是一回事。这里是连包本身都没找到。原因是 -m 是拿当前工作目录当包根去找 mpkg 这个包的,你在包里面,压根看不见自己这个包。
3.11 起可以把这个注入整个关掉
Python 3.11 加了 -P 选项和 PYTHONSAFEPATH 环境变量,作用是不再把程序文件所在目录塞进搜索路径。
实测加上之后,直接运行的 sys.path[0] 变成了解释器自带的 python313.zip,程序文件目录不在里面了,于是连跑法一那种平铺 import modx 也一起失败,报 ModuleNotFoundError: No module named 'modx'。
想省掉「哪个目录被加进搜索路径」这类推理,可以直接开这个开关,代价是程序文件目录里的同级模块不再自动可见。
没有 __init__.py 的包也能用模块方式跑
我试了一个没有 __init__.py 的目录 nspkg,里面有 modx.py 和 show.py。
在上一级跑 python -m nspkg.show,__package__ 是 'nspkg',from . import modx 成功。这种没有 __init__.py 的目录叫命名空间包,模块方式照样能定位到它。
同一个文件直接跑,还是那句 attempted relative import with no known parent package。
排查顺序
遇到 ModuleNotFoundError 但文件明明在,先打这三行:
import os, sys
print(os.getcwd())
print(sys.path[:3])
print(__package__)
os.getcwd() 告诉你人在哪,sys.path 前三条告诉你会去哪找,__package__ 告诉你相对导入有没有锚点。再确认一下用的是哪种启动方式。
同一份代码在两种启动方式下,搜索路径和包上下文都不一样,这是「我这儿能跑你那儿不行」最常见的一个来源。
- 点赞
- 收藏
- 关注作者
评论(0)