问题
今天测试 xnote 在Python 2.7兼容性的时候,发现一个功能不能使用了,但是Python3下面却运行很好。
具体表现是这样,我有一个search模块,它会去加载search目录下的子模块并且把它们注册到一个映射表中,用户输入查询条件之后会通过映射表的pattern匹配,匹配上了就执行相应的方法执行搜索动作。写成伪代码如下
mappings = []
reg_infos = [
(r"([a-zA-Z]+)", "translate.search"),
(r"(.*[0-9]+.*)", "calculator.search")
]
def init():
for pattern, modname in reg_infos:
mod = import_mod(modname)
mappings.append((pattern, mod.search))
def search(key):
result = []
for pattern, func in mappings:
if re.match(pattern, key):
result += func(key)
return result
有一个很奇怪的现象是 Python2.7 下面calculator执行很正常,但是translate不正常, Python 3.x一切正常。打印出来的错误栈如下
AttributeError: 'NoneType' object has no attribute 'path'
内心OS:小case,待我分分钟解决。
排查过程
这就很神奇了,我已经import了os模块,为了验证具体情况,我加了一些日志重现这种情况
# translate.py
import os
print("step1", os)
def do_the_job(key)
...
def search(key):
print("step2", os)
do_the_job(key)
重新执行一遍,结果输出的是
step1 <module 'os' from 'C:\\Python34\\lib\\os.py'>
step2 None
这下我傻眼了,还有这种操作???于是我一项项检查
- 有没有手动设置?No
- 是不是相对路径混淆了? 没找到os为名的模块,No
- 在函数体里面import行不行呢?OK
似乎找到了一个办法,先这样tricky的解决一下吧,我想。不幸的是python又给我抛出一个错误
File "...translate.py" line XX, in search
do_the_job(key)
TypeError: 'NoneType' object is not callable
心好累,我揉了揉被打肿的脸。看看大神们有没有遇到这种情况的吧,Google搜了一下,还真有,Stack Overflow上有一个问题 https://stackoverflow.com/questions/17084260/imported-modules-become-none-when-running-a-function 原来是垃圾回收器回收了这个模块,受到这个问题的启发,我马上意识到了问题所在:
xnote 设计的模块管理都是通过xmanager来控制的,所有的模块也都是由xmanager自己扫描加载的,为了获得最新的版本,xmanager会检查sys.modules中是否已经有这个模块,如果有的话会先把它删除。由于xmanager和search都会加载这写模块,会造成一些不一致的情况。
通过把del sys.modules[modnam]
操作记录到日志中,问题原因就清楚了
- xmanager加载calculator.py
- xmanager加载search.py
- search.py 加载calculator.py, translate.py 我们记录为calculator_v1, translate_v1
- xmanager记载translate.py,它发现这个已经在sys.modules里面了,于是删除重新加载,记为translate_v2
修改的方式也很多,我改成了使用延迟加载的方式,先让xmanager把所有模块加载完,然后再由search去加载xmanager已经加载好的搜索模块。这个问题让我想起了Java的classloader双亲委派机制,要么让parent去加载模块,要么自己加载,千万不要两个都去加载。
现在还有一个小问题,为什么python2.7和python3.x对待之前的translate_v1行为不一致?
我们再来做一个小实验
# a.py
import os
print('step1', os)
def test():
print('step2', os)
# b.py
import sys
import a
func = a.test
del sys.modules['a']
# 如果a不重置为None,Python2和3的表现是一致的,所以差异性主要在GC行为上面
a = None
func()
使用python2.7运行结果如下
('step1', <module 'os' from 'C:\Python27\lib\os.pyc'>)
('step2', None)
python3.4
step1 <module 'os' from 'C:\\Python34\\lib\\os.py'>
step2 <module 'os' from 'C:\\Python34\\lib\\os.py'>
结论
- 谨慎使用多个加载机制,保证一个模块只被加载一次
- python2.x和3.x在
del sys.modules[name]
行为上有所不同,python2.x会把删除的模块上的全局变量置为None,python3.x不会,这主要是GC行为的改变导致的 (TODO 还需要深入了解相关的规范)