Python reload 重载模块:函数更新了,旧引用为什么仍可能执行旧代码
修改源文件以后,谁真正变了
在交互式调试里修改模块,再调用 importlib.reload,往往能继续试验。但重载不是重新启动解释器:模块内的名称会重新绑定,其他地方保存的函数引用未必跟着更新。更容易误判的是,旧函数也可能读到新全局变量,于是输出同时带着新数据和旧逻辑。遇到这种混合状态,单看文件是否已保存很难解释。
下面在 Python 3.12.14 上验证。把完整代码保存为 demo.py,用 python demo.py 运行。它只在自动清理的临时目录创建模块,结束时恢复搜索路径。为了让两次写入直接对应源代码,实验暂时关闭字节码文件写入;这只是控制实验条件,不是生产部署建议。
保留一份旧引用,再重载
第一版模块定义 VALUE、EXTRA 和 read。主程序先导入模块,并把 read 单独保存在 old_read 中。第二版改变 VALUE 和函数实现,同时删除 EXTRA 的定义。随后 reload 在已有模块命名空间里执行第二版,代码再分别调用模块属性和旧引用,检查三件事是否一起改变。
AI概念示意图:新旧函数卡片并存,外部仍握着旧引用,旧名称可能继续留存。图片用于说明概念,不是运行截图。
import importlib
import sys
import tempfile
from pathlib import Path
old_flag = sys.dont_write_bytecode
sys.dont_write_bytecode = True
try:
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "reload_demo18.py"
path.write_text(
"VALUE = 'old'\nEXTRA = 'retained'\n"
"def read():\n return VALUE, 'old-code'\n",
encoding="utf-8")
sys.path.insert(0, folder)
try:
mod = importlib.import_module("reload_demo18")
old_read = mod.read
path.write_text(
"VALUE = 'new'\n"
"def read():\n return VALUE, 'new-code'\n",
encoding="utf-8")
importlib.invalidate_caches()
mod = importlib.reload(mod)
assert mod.read is not old_read
assert mod.read() == ("new", "new-code")
assert old_read() == ("new", "old-code")
assert mod.EXTRA == "retained"
print("module:", mod.read())
print("saved:", old_read())
print("extra:", mod.EXTRA)
finally:
sys.path.remove(folder)
sys.modules.pop("reload_demo18", None)
finally:
sys.dont_write_bytecode = old_flag旧函数与新数据可以同时出现
输出依次是 module: (new, new-code)、saved: (new, old-code) 和 extra: retained,实际打印时字符串带引号。第一行说明模块属性已指向新函数。第二行说明 old_read 仍保存旧函数对象,其代码没有替换;但函数查找全局变量时使用的模块字典被保留,VALUE 已变成 new。因此旧引用并不等于一份完整的旧运行环境。
第三行说明,从源文件删去定义,不会自动从已有模块字典删除名称。例子中的 EXTRA 继续存在。如果只验证新函数返回值,便会漏掉这种残留。检查重载效果时,应同时看函数对象身份、调用结果以及本该消失的名称,不要只看 reload 有没有抛异常。
什么时候应重新开始进程
这个实验使用普通纯 Python 模块,不涵盖带初始化状态的扩展模块。重载还会重新执行顶层语句;顶层若注册事件、开启连接或修改其他对象,重复执行可能留下额外状态。调用方如果使用 from module import read,持有的同样是当时绑定的引用,需要重新导入才会取得新对象。
如果目的是核验一次全新启动的行为,启动新的解释器进程更容易得到可解释的结果。确实需要交互式重载时,尽量通过 module.read 访问,并明确清理哪些状态。已经建立的类实例也不会因为定义模块重载就自动换成新类;不要把小型调试便利扩展成对整个运行中应用的无缝升级承诺。
实际排查时,可以把本例的三项检查改成项目自己的验收点:新入口是否返回新结果,已缓存的回调是否仍用旧实现,移除的配置名是否仍能被访问。先在单线程小样本重现,再决定是否使用重载;重载操作本身不是线程安全的。实验最后删除测试模块在模块缓存中的条目,只是收尾步骤,不会收回其他地方已经持有的对象引用,也不能当作清空整个应用状态的方法。


