模块搜索路径
当代码中写下 import spam 时,Python 解释器需要定位名为 spam 的模块。这个过程遵循一套明确的搜索规则,理解这些规则有助于排查 ModuleNotFoundError,也能避免本地文件意外覆盖标准库。
搜索顺序:内置优先,目录次之
模块查找分为两步。第一步检查内置模块——那些直接编译进解释器、没有对应 .py 文件的模块,例如 sys、builtins。它们的名称可以通过 sys.builtin_module_names 查看:
import sys
print('sys' in sys.builtin_module_names) # True
print('os' in sys.builtin_module_names) # False,os 是标准库而非内置
print('json' in sys.builtin_module_names) # False
如果目标不是内置模块,解释器进入第二步:在 sys.path 列表中按顺序搜索 spam.py(或包目录)。sys.path 在解释器启动时从以下位置初始化:
- 被命令行直接运行的脚本所在目录(或未指定文件时的当前工作目录)。
PYTHONPATH环境变量中列出的目录,格式与系统PATH相同,用分号(Windows)或冒号(Unix)分隔。- 依赖于安装的默认值,通常包含标准库目录和
site-packages目录(由site模块处理)。
import sys
for i, p in enumerate(sys.path, 1):
print(f"{i}. {p}")
这个顺序有一个重要后果:如果你在脚本所在目录创建了一个与标准库同名的文件(比如 json.py),那么 import json 会优先加载你的本地文件,而不是标准库。这通常是个错误,除非你有意替换该模块。
PYTHONPATH 与动态修改
PYTHONPATH 是用户扩展模块搜索路径的主要手段。假设项目结构如下:
/home/user/
project/
main.py
libs/
helper.py
为了让 main.py 能导入 libs/helper.py,可以在启动前设置环境变量:
# Linux/macOS
export PYTHONPATH=/home/user/libs
python project/main.py
# Windows
set PYTHONPATH=C:\Users\user\libs
python project\main.py
也可以在程序运行期间直接修改 sys.path。由于它是普通列表,支持 append、insert 等操作:
import sys
sys.path.append('/home/user/libs')
sys.path.insert(0, '/home/user/experimental') # 插入到最前面,优先级最高
import helper # 现在可以找到了
但运行时修改 sys.path 通常只应在特殊场景(如插件系统)中使用,常规项目应通过虚拟环境或安装包来管理依赖。
符号链接与目录解析
在支持符号链接的文件系统中,"被命令行直接运行的脚本所在目录"指的是符号链接最终指向的目录,而不是链接本身所在的目录。例如:
ln -s /real/project/main.py /shortcut/main.py
cd /shortcut
python main.py
此时 sys.path[0] 是 /real/project,而不是 /shortcut。这一行为防止了符号链接目录被误加入搜索路径,但也意味着如果链接和源文件不在同一目录结构下,相对导入的行为可能与预期不同。
.pyc 缓存与 pycache
为了加速模块加载,Python 会把源码编译成字节码,缓存在 __pycache__ 目录中。文件名遵循 module.cpython-版本.pyc 的格式,例如 spam.cpython-312.pyc。这种命名允许不同 Python 版本的编译文件共存于同一目录,而不会互相覆盖。
myproject/
spam.py
__pycache__/
spam.cpython-312.pyc
spam.cpython-311.pyc
Python 在导入时会自动比较 .py 源码与 .pyc 缓存的修改时间。如果源码更新,则重新编译;如果只有 .pyc 而无 .py,则直接加载缓存。整个过程完全自动,无需手动干预。
但有两种情况不检查缓存:一是从命令行直接载入的模块(非导入),每次都会重新编译且不保存结果;二是只有 .pyc 没有 .py 时,不会验证缓存是否过期。后一种情况常用于以编译后形式分发代码,此时 .pyc 应放在源码目录而非 __pycache__ 中,且该目录不能存在同名的未编译源文件。
优化编译:-O 与 -OO
使用 -O 或 -OO 命令行开关可以生成优化后的字节码,减小 .pyc 文件体积:
-O:去除assert语句。-OO:去除assert语句和文档字符串(__doc__)。
优化后的缓存文件带有 opt- 标签,例如 spam.cpython-312.opt-1.pyc(对应 -O)或 spam.cpython-312.opt-2.pyc(对应 -OO)。需要注意的是,.pyc 文件只是加载更快,运行时执行速度并不会比直接从 .py 读取更快。
python -O script.py # 使用 assert 去除后的字节码
python -OO script.py # 同时去除 __doc__
如果程序依赖 assert 做运行时检查,或依赖 __doc__ 生成文档,不应使用这些选项。
批量编译:compileall
compileall 模块可以为整个目录树预生成 .pyc 文件,常用于部署时加速首次启动:
import compileall
import sys
# 编译当前目录下所有模块
compileall.compile_dir('.', force=True)
# 或从命令行执行
# python -m compileall /path/to/project
force=True 会忽略时间戳检查,强制重新编译。在 CI/CD 流程中,可以在打包阶段运行 python -m compileall,确保生产环境无需在首次导入时现场编译。
内置模块与标准库的区别
初学者常混淆"内置模块"和"标准库"。内置模块如 sys、builtins 直接链接进解释器二进制,没有独立的 .py 文件,因此不受 sys.path 影响,也无法通过修改路径来替换。标准库模块如 os、json 则以 .py 文件形式存放在安装目录中,理论上可以被同名本地文件覆盖。
import sys
print(sys.builtin_module_names[:5]) # ('_abc', '_ast', '_codecs', ...)
总结
模块搜索路径的优先级是:内置模块 > sys.path 中的目录列表。sys.path 本身由脚本目录、PYTHONPATH 和安装默认值组成,且可以在运行时修改。__pycache__ 和 .pyc 文件是自动管理的字节码缓存,用于加速加载而非加速执行。理解这些机制,就能准确定位 ModuleNotFoundError 的根源,也能避免本地文件意外屏蔽标准库。