目前项目组正在使用的热更新机制有一些潜规则,其中一个就是不能更新闭包函数(因此也就不能对函数使用装饰器修饰)。
热更新机制原理
先来说说目前的热更新机制的原理,由于更新类是一个较为复杂的话题,因此这里只讨论更新函数的情况。
当需要热更新一个函数时:
(1)首先是调用python的built-in函数reload,这个函数会把模块重编并重新执行。
(2)然后再找出所有引用了旧函数的地方,将其替换为引用新的函数。
复杂的地方在于第二个步骤,如何做到更新所有的引用呢?看看python里面函数的实现:
typedef struct {
PyObject_HEAD
PyObject *func_code; /* A code object */
PyObject *func_globals; /* A dictionary (other mappings won't do) */
PyObject *func_defaults; /* NULL or a tuple */
PyObject *func_closure; /* NULL or a tuple of cell objects */
PyObject *func_doc; /* The __doc__ attribute, can be anything */
PyObject *func_name; /* The __name__ attribute, a string object */
PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */
PyObject *func_weakreflist; /* List of weak references */
PyObject *func_module; /* The __module__ attribute, can be anything */
/* Invariant:
* func_closure contains the bindings for func_code->co_freevars, so
* PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
* (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
*/
} PyFunctionObject;
python的函数也是对象,并对应于c中PyFunctionObject结构体。因此这里有一个取巧的做法,只需要将PyFunctionObject结构体中的成员替换更新即可。
这个做法简单方便、易于实现,并且很多成员的替换可以在python层实现。现在项目组的热更新模块就是这样做的。
def update_function(old_fun, new_fun):
#更新函数的PyCodeObject
old_fun.func_code = new_fun.func_code
#更新其它
...
python中闭包的实现
先来简单的了解一下python中闭包的实现。
def dec(f):
def warp():
f()
return warp
函数dec编译后的字节码如下:
3 0 LOAD_CLOSURE 0 (f)
3 BUILD_TUPLE 1
6 LOAD_CONST 1 (<code object warp at 0000000002F71A30, file "test.py", line 3>)
9 MAKE_CLOSURE 0
12 STORE_FAST 1 (warp)
5 15 LOAD_FAST 1 (warp)
18 RETURN_VALUE
可以看到dec函数会将被内层函数warp引用到的对象(如f)包装成一个cellobject,再打包成一个tuple传递给warp。在虚拟机执行MAKE_CLOSURE指令时会通过PyFunction_SetClosure函数将这个tuple设置到PyFunctionObject结构的func_closure成员上。
为什么现在的热更新模块不支持更新闭包函数?
在了解到闭包实现之后,我们知道了在PyFunctionObject结构体上面有个成员func_closure,里面会引用住一些闭包会使用到的函数(如warp的f)。如果对函数warp热更时不替换这部分的数据,那么更新之后函数还是引用了旧的f函数!目前项目组的热更模块就是缺少对func_closure的替换。好了找到问题所在了,接下来的问题就是如何更新func_closure成员。
更新func_closure的第一次尝试
更新func_closure最直观的想法应该是这样的:
def update_function(old_fun, new_fun):
#更新函数的PyCodeObject
old_fun.func_code = new_fun.func_code
#更新闭包数据
old_fun.func_closure = new_fun.func_closure
#更新其它
...
可惜不行,func_closure是一个readonly property。
更新func_closure的第二次尝试
既然不行那我遍历tuple,更新其中的cellobject总可以了吧。遗憾的是也不行,cellobject对象身上的cell_contents是不可写的(详情参考CPython源码中的cellobject.c),代码就不放上来了。
更新func_closure的第三次尝试
这一次我是决定直接在c里面改这个指针,这样基本可以绕过python对其的限制。具体方式是用c实现一个扩展模块:
//Note Since Python may define some pre-processor definitions
//which affect the standard headers on some systems,
//you must include Python.h before any standard headers are included.
#include "Python.h"
static PyObject *
PyReload_UpdateFunctionClosure(PyObject *self, PyObject *args) {
PyObject *o1, *o2;
if (!PyArg_ParseTuple(args, "OO", &o1, &o2)) {
return NULL;
}
if (!PyFunction_Check(o1) || !PyFunction_Check(o2)) {
return NULL;
}
PyObject* closure = PyFunction_GetClosure(o2);
if (closure == NULL) {
return NULL;
}
if (PyFunction_SetClosure(o1, closure) != 0) {
return NULL;
}
Py_RETURN_NONE;
}
static PyMethodDef PyReload_Methods[] =
{
{ "update_function_closure", PyReload_UpdateFunctionClosure, METH_VARARGS, "更新python函数闭包数据" },
{ NULL, NULL, 0, NULL }/* Sentinel */
};
PyMODINIT_FUNC
initPyReload(void)
{
(void)Py_InitModule("PyReload", PyReload_Methods);
}
在python里面的更新函数可以这么写:
def update_function(old_fun, new_fun):
#更新函数的PyCodeObject
old_fun.func_code = new_fun.func_code
#更新闭包数据
if new_fun.func_closure:
import PyReload
PyReload.update_function_closure(old_fun, new_fun)
这样总算可以了。不过需要注意的是要正确处理好引用计数问题,还有就是不知道这段几十行的代码还有无别的问题。毕竟都千方百计不让你对func_closure进行修改了,或许这里面有坑,并且我没有注意到:)