背景
线上有个 Python 服务从消息队列获取数据进行处理,在每次处理处理前,会检查有哪些文件变更,针对变更的模块执行 reload 以实现代码的热更,但在一次更新后,发现变更后的代码并未热更生效。
花了半天时间排查,最终发现是代码实现上的一个小坑。
复现场景
基于原始代码逻辑简化逻辑后,复现场景共需要这些文件。
1
2
3
4
5
|
├── main.py
├── model
│ ├── model1.py
│ └── tools.py
└── P1001.py
|
各个文件内容如下。
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
# main.py
import os
import sys
import time
import importlib
class Server:
def __init__(self):
self.policy_module = []
def reload_code(self):
module_files = set()
path_list = ["."]
# 遍历当前目录下的所有py代码
while len(path_list) > 0:
path = path_list.pop()
for file_name in os.listdir(path):
if os.path.isdir(file_name):
new_path = path + "/" + file_name
path_list.append(new_path)
if new_path not in sys.path:
sys.path.append(new_path)
else:
if file_name.endswith(".py") and file_name != "main.py":
abs_path_name = path + "/" + file_name
module_name = abs_path_name.replace("./", "").replace(".py", "").replace("/", ".")
module_files.add(module_name)
print(module_files)
# 如果已经加载过,那么就reload
for k in (set(sys.modules.keys()) & module_files):
print("reload %s" % k)
importlib.reload(sys.modules[k])
# 如果没有加载过,那么就加载
for k in (module_files - set(sys.modules.keys())):
print("import %s" % k)
module = importlib.import_module(k)
if k.startswith("P"):
self.policy_module.append(module)
def on_recv(self, msg):
# 每次处理数据前reload下
self.reload_code()
# 遍历所有策略模块进行处理
for module in self.policy_module:
module.policy(msg)
if __name__ == "__main__":
s = Server()
# 设置tools的print代码前缀是 >>
with open("model/tools.py", "w") as f:
f.write('def m_print(s): print(">>", s)')
# 模拟处理第一个数据
s.on_recv("test aaa")
time.sleep(2)
print()
# 设置tools的print代码前缀是 --
with open("model/tools.py", "w") as f:
f.write('def m_print(s): print("--", s)')
# 模拟处理第二个数据
s.on_recv("test bbb")
|
P1001.py
1
2
3
4
5
6
|
# P1001.py
import model.model1 as m
def policy(msg):
m.predict(msg)
|
model1.py
1
2
3
4
5
|
# model1.py
from tools import *
def predict(data):
m_print(data)
|
tools.py
tools.py
文件是空文件,在 main.py
中测试时,将代码内容写入到 tools.py
中。
上述代码中,只有 main.py
代码的逻辑稍微复杂些,大体来说,
- 在框架收到数据时,将会调用
Server
类实例的 on_recv
方法。
- 在
on_recv
方法中,首先会遍历当前目录下的所有代码,如果已经加载过,那么就会 reload,如果没有加载过,就会加载。同时存储所有 P
开头的模块。
- 遍历所有
P
开头的模块,并调用其 policy
方法。
- 在
P10001.py
代码中,调用了 model.model1
模块的 predict
方法,而在 model1
的 predict 方法中,则又调用了 tools
模块的 m_print
方法。
- 整个程序调用了两次
on_recv
方法,在这两次调用前,修改 tools
模块中 m_print
方法的打印前缀,第一次是 >>
,而第二次是 --
。
- 理论上来说,第一次打印数据时,输出的前缀应该是
>>
,而第二次打印数据时,输出的前缀应该是 --
。
但实际程序的执行结果如下,两次打印的前缀均为 >>
。线上的问题与此类似,热更了 tools.py
的代码,但实际上并未生效。

原因
经过定位,核心原因在 model1.py
中的 from tools import *
这一句。
在程序处理完第一个数据后,查看 Python 进程中的模块,会发现有 2 个 tools
模块,分别是 model.tools
、tools
。

原来在 main.py
中加载模块时是基于路径来的,其加载的模块时 model.tools
,热更的也是 model.tools
。但在 model.model1
中,却直接导入了 tools
(main.py
中在遍历 py 文件时,顺便也将路径都加入到了 sys.path
中,因此可以直接导入 tools
),对于 Python 来说,这两个模块处于不同的命名空间下,也即是两个模块。
而 main
的更新逻辑更新的只是 model.tools
,并不是 tools
,所以这次热更并未生效。
尝试解决
最直接的解决办法是修改 model.model1
模块的导入逻辑,将其改为 from model.tools import *
,这样 model.model1
使用的 tools
模块就与框架更新逻辑的一致了。
修改后的 model.model1
模块代码。
1
2
3
4
|
from model.tools import *
def predict(data):
m_print(data)
|
但在实际测试中,发现这个修改时而生效,时而不生效。经过仔细对比,发现生效不生效与 reload
模块时的顺序有关。

当先 reload model.model1
模块时,将不生效,而先 reload model.tools
模块时则生效。
询问 kimi 得知 from a.b import *
的方式是将模块中的方法导入到当前命名空间中,必须刷新当前命名空间后才能生效。先 reload model.tools
之后再 reload model.model1
则保证了 model.model1
命名空间的刷新。

彻底解决
为了避免 reload
顺序导致热更失效的问题,最好还是不要使用 from aa import *
或 from aa import abc
的方式,而是直接导入整个模块,然后再去使用模块的方法。
这是修改后的 model.model1
的代码。
1
2
3
4
|
import model.tools as t
def predict(data):
t.m_print(data)
|
此时,无论是哪种 reload
顺序,都不影响热更的效果。