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.cfunc_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就可以了。不过是不是还有什么东西没写到呢?就是如何找到需要更新的模块。不过有点写不动了,之后再慢慢总结吧。

在遇到热更新这一问题的时候,先是各种搜索,当找到一些实现之后,阅读其代码并尝试理解。但这样未必弄得明白,这种情况下自己尝试一步步来实现,那么对问题可以有更深的认识,同时也更容易理解那些已有实现中的代码究竟为何要那样实现。

参考