关于不能对闭包函数进行热更新的问题
目前项目组正在使用的热更新机制有一些潜规则,其中一个就是不能更新闭包函数(因此也就不能对函数使用装饰器修饰)。
热更新机制原理
先来说说目前的热更新机制的原理,由于更新类是一个较为复杂的话题,因此这里只讨论更新函数的情况。
当需要热更新一个函数时:
(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进行修改了,或许这里面有坑,并且我没有注意到:)