python执行脚本时导包错误如何解决
先放解决办法:
- 将未定位的包的上级目录加入到 sys.path 中。
- 使用 python -m xxx.yyy 的方式执行模块。
一、导包报错现象展示
假如我们有以下目录结构:
codes
│ main.py
│
├───db
│ service.py
│
└───utils
excel.py
内容如下图所示,我们在 codes 目录下执行 python main.py
是可以正常运行的。
但是我们想直接执行 codes/db/service.py
脚本却报错了,如下图所示。
(在 codes 目录下执行报错,在 codes/db 目录下执行报错,使用vscode的调试模式执行也是报错。)
二、导包报错现象溯源
2.1 溯源过程
导包报错的提示很明确: ModuleNotFoundError: No module named 'utils'
也就是没找到这个包,如果要定位为什么没找到这个包,那我们就需要知道执行脚本时,会去哪里找我们 import 命令指定的包。
根据 python 官方文档 The import system 中所述,import 会去 sys.path 中检索我们要导入的包,如果没找到就会报导包错误。
那我们在脚本中添加输出 sys.path 的代码,再执行脚本,对比不同。
我们发现当我们执行 python xxx.py
的时候,python 会将脚本所在目录添加到 sys.path 中,如果执行的命令是 python xxxx/yyy.py
,则添加到 sys.path 的值是 ./xxx
。
以上述代码为例,当我们直接执行 python main.py
的时候,python 将 codes
添加到了 sys.path 中,在这个目录中是可以找到我们 from utils import excel
中的 utils
包的,所以可以正常执行,而 python db/service.py
则是将 ./db
添加到了 sys.path 中,这个时候 sys.path 所有的目录下都没有 utils 这个包,所以报错。
2.2 解决方案
- 使用
python -m xxx/yyy.py
格式执行脚本(为这种情况量身定做。)
在 Python 中,-m 参数用于将模块作为脚本运行,其核心作用是
- 改变 Python 的导入行为,确保模块能以正确的上下文执行。
- 自动添加当前目录到 sys.path:确保模块能正确导入同级或父级包。
- 避免相对路径问题:解决直接运行 python xxx/yyy.py 时可能出现的导入错误。
- 在模块中手动更新 sys.path,将指定目录添加到其中。
三、Python 的导包机制
3.1 常规包和命名空间包
常规包是目录中包含 __init__.py
文件的包。
命名空间包是目录中不包含 __init__.py
文件的包。
python3.2(PEP0420)版本之前,无法直接导入非常规包,即目录中不包含 __init__.py
文件的时候,无法正常识别为 Python 包。
PEP 420 – Implicit Namespace Packages 定义了命名空间包,使得包中没有 __init__.py
文件时也可识别为包,并正常导入。所以常规包和命名空间包在根本上没有什么不同,只是识别导入的方式不同,而识别导入也是 Python 自动处理的,我们无需干涉,所以对我们来说,用起来没有区别。
3.2 导包逻辑
以导入 foo 包为例:
- 如果找到
/foo/ __init__.py
,则一个常规包被导入并返回。 - 如果没有,但是
/foo.{.py, .pyc, .so, .pyd} 被找到,则一个模块被导入并返回。 - 如果没有,但是
/foo 存在并且是一个目录,记录并继续扫描。 - 否则继续扫描。
如果扫描结束但是没有返回模块或包,并且记录了至少一个目录,则创建一个命名空间包。
3.3 常规包和命名空间包的细微区别
- 命名空间包和常规包功能上没有区别。
- 命名空间包下不能有
__init__.py
,否则就会被识别成常规包。 - 命名空间包的
__path__
属性是一个可迭代对象,里面记录着扫描过程中记录的目录信息,而常规包是一个普通列表。 - 命名空间包没有
__file__
属性,而常规包次属性是指向__init__.py
文件的值。 - 命名空间包的子模块可以来自不同路径,但是常规包只能在一个目录中。
3.4 命名空间包模块的多来源特性
Namespace 包(PEP 420)的核心用途是 允许一个逻辑上的包分散在多个物理目录,适用于模块化、插件化架构或代码分发的场景。
简单代码示例如下。