有时候我们可能需要只修改一部分代码而且要求修改立即生效,或者为了高可用性不允许停止服务程序,这时我们就需要热补丁。
在debian,red hat等系统(或者vista之后的windows)的软件更新时,通常使用替换符号链接来达到高可用性。
对Python来说,解释器预先处理了脚本生成字节码,并读入内存;所以之后硬盘上的文件发生了什么变化,就只能想办法命令解释器重新读入新的脚本。实现这个功能的内建命令是reload
以下例子的语法以Python2.x为准,但是除了语法外基本思想与Python3.x通用。了解语法差异,请查看本文的Python3节
使用reload
When reload(module) is executed:
- Python modules’ code is recompiled and the module-level code reexecuted, defining a new set of objects which are bound to names in the module’s dictionary. The initfunction of extension modules is not called a second time.
- As with all other objects in Python the old objects are only reclaimed after their reference counts drop to zero.
- The names in the module namespace are updated to point to any new or changed objects.
- Other references to the old objects (such as names external to the module) are not rebound to refer to the new objects and must be updated in each namespace where they occur if that is desired.
也就是说reload更新了命名空间中对模块名称的指向。我们来试一下:
1 2 3 |
class class_1: def __init__(self): print('class_1') |
在解释器中输入
1 2 3 |
>>> import class_1 >>> a=class_1.class_1() 'class_1' |
修改磁盘上的文件成
1 2 3 |
class class_1: def __init__(self): print('class_1_mod') |
在解释器中输入
1 2 3 |
>>> reload(class_1) >>> a=class_1.class_1() 'class_1_mod' |
这的确是我们想要的结果。
有什么问题呢
继承关系
在上面这个简单的例子里我们丝毫没有发现问题,代码运行得很好。但是考虑这种情况:
1 2 3 4 5 6 7 8 9 10 |
class super_class: def __init__(self): print('super init') class sub_class(super_class): def __init__(self): print('sub init') print(issubclass(self.__class__, super_class)) # 或者,调用父类的构造函数 # super_class.__init__(self) |
假设保存在test.py中。然后假设另一模块run.py调用了test:
1 2 3 4 5 6 7 |
import test from test import sub_class sub_class() print("") reload(test) sub_class() |
这样当我们运行run.py时,会依次输出:
sub init
Truesub init
False
可见reload之后,isinstance得到了不是我们想要的结果。通过id打印内存可以解释这个问题,我们在test.py的isinstance前加一行
1 |
print("super_class: 0x%08X" % id(super_class)) |
用同样的方法,在run.py中打印sub_class,得到输出:
test.sub_class at 0x029385A8
sub init
super_class: 0x028DEFB8
Truetest.sub_class at 0x029385A8
sub init
super_class: 0x0291B340
False
由此可见,reload(sub_class)之后,test下的所有类被重新加载,内存位置发生了变化。假设旧的模块还没被GC掉,这时内存中同时存在了新老两个版本的test模块,以及模块中的所有变量。为了方便起见,我们讲老test模块中类统一称作老xxx,新test模块中的类成为新xxx。
reload(test)之后,run.py模块的命名空间中,sub_class仍然指向旧sub_class(两次的id值一致),而从sub_class的构造函数中引用的super_class的内存位置却发生了变化。新sub_class是新super_class的子类,旧sub_class是旧super_class的子类,但旧super_class不能是新super_class的子类。
为了方便解释,我们可以画一个内存的引用图来解释这个变化:
(还没画)
要解决这个问题,可以在reload模块后再执行一次from xxx import yyy,或者直接用模块名.类名的方式引用,这样始终使用同一版本的super_class和sub_class,也就不会出现问题:
1 2 3 4 5 6 7 8 9 10 11 |
import test from test import sub_class sub_class() print("") reload(test) # 方法1 from test import sub_class sub_class() # 方法2 test.sub_class() |
灰度发布
和上面的问题相同,我们也可以通过使用之前提到的两种方法来保持使用的父类和子类始终是配套的。
但是有时的情况是,我们希望同时存在保持新旧两套版本,比如,灰度发布时,系统中可能同时存在两套代码。这时能不能在老sub_class中继续引用到老super_class呢。
- 检查是否是子类的方法有一点黑科技,看一下父类的类名在不在__bases__里就可以了。因为一个模块下同样的类名只会出现一次,所以这种方法是可靠的。
- 调用父类方法,我们可以使用super()函数,前提是类是新式类(new-style classes),只要最大辈分的类继承object就可以了。将test.py修改如下:
1 2 3 4 5 6 7 8 9 10 11 |
class super_class(object): def __init__(self): print('super init') class sub_class(super_class): def __init__(self): print('sub init') print("super_class: 0x%08X" % id(super_class)) print('super_class' in map(lambda x:x.__name__, self.__class__.__bases__)) # 或者,调用父类的构造函数 # super(self.__class__, self).__init__() |
如果最大辈分的类无法修改,可以在子类中继承object,即
1 2 3 4 5 |
# super_class is imported from somewhere w class sub_class(super_class, object): def __init__(self): super(self.__class__, self).__init__() |
Python3
在Python3.x中,test.py的super函数不需要传递参数:
super().__init__()
reload移动到了imp中,使用reload前需要先:
from imp import reload
随便扯两句
这篇文章其实最早写于2014年5月,写了一段之后不想写了2333,那个时候MAClient的网页版刚完成了一个热更新的commit,意味着可以不停止老用户的实例而更新新版本的代码进去。前两天有基友跟我说起这个脚本,跑了一下竟然还能跑,看了一下里面快满出来的红茶绿茶,还是有很多回忆(我还有几个没有公开的刷道具bug呢哈哈哈哈哈):
MAClient是我到现在为止写过的最大的一个项目,一共大概有1万行左右。很多新的构想都是从这里开始来的,比如这个热更新,以及插件的思想。没错我就是在装逼
以后写个关于插件的文章吧,感觉蛮有意思的