Python代码热更新可以找到实际可用的实现,比如IPython的autoreload.py、PyDev的pydevd_realod.py。
但如果换成自己实现,怎么从头来考虑这个问题呢?
什么是热更新
简单来说,热更新就是在进程不重启的情况下,让其加载修改后的程序代码,且能按照预期正确执行。在实际开发中,热更新的最主要用途有,
- 开发期,提升开发效率,让代码改动立等可见,避免频繁重启
- 运维期,在服务端不断线情况下修复紧急bug
对于bug修复来说,服务端如果不保持状态,那么热更新的必要性不那么大,但如果服务端持有复杂状态,那么热更新就会是一个比较合适的选择。
热更新的要点
Python代码是以module进行组织的,代码热更新就是module的热更新。
Python提供的内置函数reload用于重新加载模块,然而直接使用reload并不能够解决热更新这一问题。热更新的要点在于需要让已经创建的对象能够执行更新以后的代码。这也是autoreload.py、pyded_reload.py中大部分代码实现的目的所在。
更新操作拆解
更新普通函数
整个热更新逻辑中,函数的更新是最为重要的,因为函数是具体逻辑的执行单元。参照上述实现,定义函数更新的实现如下,
def update_function(old_func, new_func):
old_func.__doc__ = new_func.__doc__
old_func.__dict__ = new_func.__dict__
old_func.__defaults__ = new_func.__defaults__
old_func.__code__ = new_func.__code__
上述函数可以用简单样例进行验证,
def old_foo():
return 'old_foo'
def new_foo():
return 'new_foo'
class ReloadTest(unittest.TestCase):
def test_update_function(self):
self.assertEqual('old_foo', old_foo())
update_function(old_foo, new_foo)
self.assertEqual('new_foo', old_foo())
更新decorator修饰的函数
目前的实现可以通过上面的测试用例,与pydevd_reload.py中的_update_function是一致的,但根据pydevd_reload.py的注释,
Functions and methods using decorators (other than classmethod and staticmethod) are not handled correctly.
也就是说这样的实现是不支持decorator的,因此扩充下用例,
def decorator(func):
def _(*args, **kwargs):
return func(*args, **kwargs)
return _
@decorator
def old_foo_with_decorator():
return 'old_foo'
@decorator
def new_foo_with_decorator():
return 'new_foo'
class ReloadTest(unittest.TestCase):
def test_update_function_with_decorator1(self):
self.assertEqual('old_foo', old_foo_with_decorator())
update_function(old_foo_with_decorator, new_foo_with_decorator)
self.assertEqual('new_foo', old_foo_with_decorator())
def test_update_function_with_decorator2(self):
self.assertEqual('old_foo', old_foo())
update_function(old_foo, old_foo_with_decorator)
self.assertEqual('new_foo', old_foo())
上述两个case都会失败。先来解决第一个case失败的情况,当需要更新的函数都被decorator修饰的时候,update_function并没有发生作用,这种情况可以通过递归进行处理,修改update_function如下,
def both_instance_of(first, second, klass):
return isinstance(first, klass) and isinstance(second, klass)
def update_function(old_func, new_func):
old_func.__code__ = new_func.__code__
old_func.__defaults__ = new_func.__defaults__
old_func.__doc__ = new_func.__doc__
old_func.__dict__ = new_func.__dict__
if not old_func.__closure__ or not new_func.__closure__:
return
for old_cell, new_cell in zip(old_func.__closure__, new_func.__closure__):
if not both_instance_of(old_cell.cell_contents, new_cell.cell_contents, types.FunctionType):
continue
update_function(old_cell.cell_contents, new_cell.cell_contents)
被decorator修饰的函数可以通过最终返回函数的free variable找到,因此可以递归更新函数的__closure__来进行处理。
第二个case会遇到如下异常,
ValueError: _() requires a code object with .. free vars, not ..
之所以会抛这个异常,是因为Python在funcobject.c
的func_set_code
函数中进行了强制检查,如果不修改Python源码的话,应该是绕不过去了。因此update_function需要稍作调整,这种情况下不进行更新,而不抛异常,
def update_function(old_func, new_func):
if not both_instance_of(old_func, new_func, types.FunctionType):
return
if len(old_func.__code__.co_freevars) != len(new_func.__code__.co_freevars):
return
old_func.__code__ = new_func.__code__
old_func.__defaults__ = new_func.__defaults__
old_func.__doc__ = new_func.__doc__
old_func.__dict__ = new_func.__dict__
if not old_func.__closure__ or not new_func.__closure__:
return
for old_cell, new_cell in zip(old_func.__closure__, new_func.__closure__):
if not both_instance_of(old_cell.cell_contents, new_cell.cell_contents, types.FunctionType):
continue
update_function(old_cell.cell_contents, new_cell.cell_contents)
更新类
在处理完函数更新之后就可以来实现类更新了,类更新涉及普通成员函数、类函数、静态函数、property的更新,同时需要对属性增删进行处理。
def update_class(old_class, new_class):
for name, new_attr in new_class.__dict__.items():
if name not in old_class.__dict__:
setattr(old_class, name, new_attr)
else:
old_attr = old_class.__dict__[name]
if both_instance_of(old_attr, new_attr, types.FunctionType):
update_function(old_attr, new_attr)
elif both_instance_of(old_attr, new_attr, staticmethod):
update_function(old_attr.__func__, new_attr.__func__)
elif both_instance_of(old_attr, new_attr, classmethod):
update_function(old_attr.__func__, new_attr.__func__)
elif both_instance_of(old_attr, new_attr, property):
update_function(old_attr.fdel, new_attr.fdel)
update_function(old_attr.fget, new_attr.fget)
update_function(old_attr.fset, new_attr.fset)
elif both_instance_of(old_attr, new_attr, (type, types.ClassType)):
update_class(old_attr, new_attr)
不过类上面的__slots__
、__metaclass__
如果发生了变化,也是无法正确更新的。
更新模块
模块上的更新也是类似类更新处理,这里只处理通常情况下模块内可能直接存在的类型,
def update_module(old_module, new_module):
for name, new_val in new_module.__dict__.iteritems():
if name not in old_module.__dict__:
setattr(old_module, name, new_val)
else:
old_val = old_module.__dict__[name]
if both_instance_of(old_val, new_val, types.FunctionType):
update_function(old_val, new_val)
elif both_instance_of(old_val, new_val, (type, types.ClassType)):
update_class(old_val, new_val)
定义回调接口
一路分析下来可以看到热更新不仅有不少限制,且有一些问题是没有进行处理的,
- 定义在模块或类上的属性没有进行处理
- 新增成员属性没有进行处理
- 无法在更新发生时执行某种操作
因此需要在合适的实际执行一些固定调用,好让上层逻辑能介入更新流程,进行特定处理来达成实际需求。
模块更新回调,可以在update_module的最后增加处理,约定回调的函数名称,
def update_module(old_module, new_module):
...
if hasattr(old_module, '_on_reload_module'):
old_module._on_reload_module()
类更新回调,在update_class最后进行处理,
def update_class(old_class, new_class):
...
if hasattr(old_class, '_on_reload_class'):
old_class._on_reload_class()
就像pydevd_reload.py里约定的__reload_update__
这个hook函数一样,一路思考下来就可以理解为什么它需要定义这样的函数。
如果在__init__
里面增加了属性定义,对于旧对象来说__init__
是不会再次执行的,因此没有机会创建属性。所以一般想以热更新方式更新代码时需要注意避免这种情况,如果实在绕不开,那么有两种处理思路,一是在使用新属性的时候都使用getattr,作为临时的workaround,勉强能接受。另外一种则是在更新时找到之前创建的所有对象,主动给对象设置新属性的初始值。寻找类实例可以通过gc模块,
def update_class(old_class, new_class):
...
if hasattr(old_class, '_on_reload_instance'):
for x in gc.get_referrers(old_class):
if isinstance(x, old_class):
x._on_reload_instance()
总结
在实现上述函数之后,就有了代码热更新的执行逻辑了,找到需要更新的模块,然后调用update_module就可以了。不过是不是还有什么东西没写到呢?就是如何找到需要更新的模块。不过有点写不动了,之后再慢慢总结吧。
在遇到热更新这一问题的时候,先是各种搜索,当找到一些实现之后,阅读其代码并尝试理解。但这样未必弄得明白,这种情况下自己尝试一步步来实现,那么对问题可以有更深的认识,同时也更容易理解那些已有实现中的代码究竟为何要那样实现。