python通过源码元数据摘要与编译器版本标识双校验决定.pyc是否重编译:检查magic number、source_hash及python版本;不匹配则丢弃旧缓存,改代码未生效可能是hash巧合碰撞或权限问题导致缓存未更新。

Python 怎么判断 .pyc 文件该不该重编译
Python 不靠文件修改时间戳做唯一判断,而是用「源码元数据摘要 + 编译器版本标识」双校验。具体来说,.pyc 文件头部会存一个 16 字节的 magic number(含 Python 版本主次号),后面紧跟着源文件的 mtime 和 size 的字节级哈希(CPython 3.7+ 改用 8 字节 source_hash)。只要源文件内容变了、Python 升了小版本(比如 3.9.7 → 3.10.0)、甚至只是用了不同构建的解释器(如 pyenv 编译版 vs 官方二进制版),.pyc 就会被丢弃。
常见错误现象:ImportError: bad magic number 就是 magic 不匹配;而明明改了代码却没生效,往往是 __pycache__ 下残留了旧 .pyc,且它的 hash 恰好和当前源码“撞车”(极小概率,但真实存在)。
- 不要手动 touch 修改
.py文件时间戳来“骗过”缓存——hash 校验不认这个 - 跨 Python 版本运行时,
__pycache__目录名含版本号(如__pycache__/main.cpython-39.pyc),不会误用,但旧版本残留可能干扰调试 - 使用
compileall预编译时,加-f强制覆盖,否则跳过已存在的.pyc
import 时找不到对应 .pyc 会怎样
找不到就现场编译,生成新 .pyc 并继续导入——整个过程对用户透明。但要注意:这发生在模块首次被 import 的时刻,不是脚本启动时;而且如果当前用户对 __pycache__ 目录无写权限,Python 会静默跳过写缓存(不报错),后续每次 import 都重新编译源码,拖慢启动。
使用场景:容器里只读文件系统、CI 构建环境临时目录、某些嵌入式 Python 运行时。
立即学习“Python免费学习笔记(深入)”;
- 检查
sys.pycache_prefix(3.12+)或sys.path中源码所在路径的__pycache__是否可写 - 用
python -B启动可完全禁用.pyc生成(但不会删除已有缓存) -
importlib.util.cache_from_source()可查某.py对应的.pyc路径,方便诊断
多环境共用同一代码目录时的 .pyc 冲突
多个 Python 解释器(不同版本、不同用户、virtualenv/conda 环境混用)写同一个 __pycache__ 目录,会导致 .pyc 文件被覆盖或权限异常,轻则缓存失效,重则触发 PermissionError 或 OSError 导致 import 失败。
典型场景:团队共享 NFS 目录开发、Docker volume 挂载源码、WSL 与 Windows 双端编辑同一项目。
- 设环境变量
PYTHONPYCACHEPREFIX=/tmp/pycache-$(python -c "import sys; print(sys.version_info[:2])"),让各环境写各自缓存根目录 - 在
.gitignore里加**/__pycache__/和**/*.pyc,避免提交缓存文件 - 不要在部署包里打包
__pycache__——生产环境应由目标机器首次运行时生成
py_compile 和 compileall 的参数差异影响缓存行为
这两个工具本质都是调 py_compile.compile(),但默认行为不同:py_compile 默认不写 __pycache__(除非显式传 dfile),而 compileall 默认走标准缓存路径。更关键的是,它们都支持 -d(指定 .pyc 输出目录)和 -l(不递归子目录),但忽略 sys.dont_write_bytecode 设置——即使你代码里写了 sys.dont_write_bytecode = True,命令行工具仍会写。
性能影响:批量预编译能减少首次启动延迟,但若源码频繁变更,预编译反而浪费 IO;且 compileall -j N 多进程并发时,对磁盘压力明显增大。
- CI 打包建议用
compileall -b -f -q(生成 .pyc 到同目录、强制覆盖、安静模式) -
py_compile.compile(source, doraise=True)在代码中调用时,失败会抛py_compile.PyCompileError,不是普通Exception - 注意
compileall的-i参数读取文件列表时,路径必须是绝对路径或相对于当前工作目录,否则静默跳过
最易被忽略的一点:Python 从 3.8 开始,默认启用 importlib._bootstrap_external._code_to_bytecode() 的优化路径,它绕过部分校验逻辑加速加载——这意味着你在调试缓存问题时,看到的“没重编译”可能不是因为缓存命中,而是根本没走缓存校验流程。真要确认行为,得看 importlib.util.source_hash() 的输出,而不是只盯文件时间戳。










