zoukankan      html  css  js  c++  java
  • 关于不能对闭包函数进行热更新的问题

    目前项目组正在使用的热更新机制有一些潜规则,其中一个就是不能更新闭包函数(因此也就不能对函数使用装饰器修饰)。

    热更新机制原理

    先来说说目前的热更新机制的原理,由于更新类是一个较为复杂的话题,因此这里只讨论更新函数的情况。

    当需要热更新一个函数时:

    (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进行修改了,或许这里面有坑,并且我没有注意到:)

  • 相关阅读:
    Map Wiki -- proposed by Shuo Ren
    Smart Disk -- proposed by Liyuan Liu
    ubuntu 16.04下如何打造 sublime python编程环境
    manjaro linux没有ll等命令的解决办法
    python学习-命名规则
    python-unitetest-unittest 的几种执行方式
    python-pytest学习(一)- 简介和环境准备
    Python+request+unittest学习(一)- 读取文本出现 锘 * 系列乱码错误(UTF-8 BOM问题)的原因及解决方法
    Python+Selenium框架版(十)- unittest执行方法之discover()方法
    Python+Selenium框架版(九)- unittest执行法之makeSuit()
  • 原文地址:https://www.cnblogs.com/adinosaur/p/7710393.html
Copyright © 2011-2022 走看看