博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
python基于函数替换的热更新原理介绍
阅读量:7291 次
发布时间:2019-06-30

本文共 13236 字,大约阅读时间需要 44 分钟。

热更新即在不重启进程或者不离开Python interpreter的情况下使得被编辑之后的python源码能够直接生效并按照预期被执行新代码。平常开发中,热更能极大提高程序开发和调试的效率,在修复线上bug中更是扮演重要的角色。但是要想实现一个理想可靠的热更模块又非常的困难。

1.基于reload

reload作为python官方提供的module更新方式,有一定作用,但是很大程度上并不能满足热更的需求。

先来看一下下面的问题:

>>> import sys, math>>> reload(math)
>>> sys.modules.pop('math')
>>> __import__('math')
>>> reload(math)Traceback (most recent call last): File "
", line 1, in
reload(math)ImportError: reload(): module math not in sys.modules>>> sys.modules.get('math')
>>> id(math), id(sys.modules.get('math'))(45429424, 45540272)

函数 __import__ 会在import声明中被调用。import导入一个模块分两步:

  1. find a module, and initialize it if necessary;
  2. define a name or names in the local namespace;

 其中第一步有以下的搜寻过程:a): sys.modules; b): sys.meta_path; c):sys.path_hooks, sys.path_importer_cache, and sys.path

上面例子中math从缓存sys.modules移除后,__import__会重新load math并添加到sys.modules,导致当前环境中math绑定的math module和sys.modules中不一致,导致reload失败。

热更使用reload并动态的使用__import__导入很容易犯该错误,另外reload要求模块之前已经被正确的引入。

# -*- coding:utf-8 -*-import time, os, sysimport hotfix# r = hotfix.gl_var# @singletonclass ReloadMgr(object):    to_be_reload = ('hotfix',)    check_interval = 1.0    def __init__(self):        self._mod_mtime = dict(map(lambda mod_name: (mod_name, self.get_mtime(mod_name)), self.to_be_reload))    def polling(self):        while True:            time.sleep(1)            self._do_reload()    def _do_reload(self):        for re_mod in self.to_be_reload:            last_mtime = self._mod_mtime.get(re_mod, None)            cur_mtime = self.get_mtime(re_mod)            if cur_mtime and last_mtime != cur_mtime:                self._mod_mtime.update({re_mod:cur_mtime})                ld_mod = sys.modules.get(re_mod)                reload(ld_mod)    @staticmethod    def get_mtime( mod_name):        ld_mod = sys.modules.get(mod_name)        file = getattr(ld_mod, '__file__', None)        if os.path.isfile(file):            file = file[:-1] if file[-4:] in ('.pyc', '.pyo') else file            if file.endswith('.py'):                return os.stat(file).st_mtime         return Noneif __name__ == '__main__':    reload_mgr = ReloadMgr()    reload_mgr.polling()
View Code

上面的这个例子轮询检测已经被导入过的指定模块的源代码是否被修改过,如果被修改过,使用reload更新模块。这种方式思路清晰,实现简单,然而并没有太大的实际用途。主要原因如下:

  • 通过 from mod_a import var_b 的方式在mod_c模块中引入的变量var_b并不会随着reload(mod_a)而更新,var_b将依旧引用旧值。该问题同样存在于引入的函数和类;可以重新执行from语句或者通过mod_a.var_b的方式使用var_b。显然如果在mod_c中引入了mod_a的一个类mod_a_cls并且有一个对象a_cls_obj,要想是a_cls_obj执行新代码,必须要重新生成一个新的对象。
  • 用指令触发主动的进行更新可能较为实际,避免修改错误或者只修改了若干文件中的一个就触发更新导致错误;
  • 指定检测更新模块的方式不灵活,且要求先前导入过被检测模块;
  • 更新完成后主进程被阻塞,直到下一次更新检测。

因此,本质上这个程序仅仅是用作检测文件修改并使用reload更新,根本的缺陷是旧的对象不能执行新的代码,需要重新生成新的对象。可以应用于特定少量文件的更新。

2.基于进程/线程检测

 针对上面介绍的一个例子存在的问题,可以使用进程或者线程将模块修改检测的工作和程序的执行分离开来。

大致思路就是,不直接启动主程序,而是启动一个检测程序,在检测程序中创建一个进程或者线程来执行主程序。

./MainProgram.py

1 # -*- coding:utf-8 -*- 2 import time 3 # import cnblogs.alpha_panda 4  5 cnt = 0 6  7 def tick(): 8     global cnt 9     print __name__, cnt10     cnt += 111 12 def start_main_loop():13     frame_time = 114     while True:15         time.sleep(frame_time)16         tick()17 18 def start_program():19     print 'program running...'20     start_main_loop()21 22 if __name__ == '__main__':23     start_program()
View Code

 ./Entry.py

1 # -*- coding:utf-8 -*- 2  3 import os, sys 4 import threading, time, subprocess 5 import MainProgram 6  7 class Checker(): 8     def __init__(self): 9         self._main_process = None10         self._check_interval = 1.011         self._exclude_mod = (__name__, )12         self._entry_program = r'./MainProgram.py'13         self._mod_mtime = dict(map(lambda mod_name: (mod_name, self.get_mtime(mod_name)), sys.modules.iterkeys()))14         self._start_time = 015 16     def start(self):17         self._initiate_main_program()18         self._initiate_checker()19 20     def _initiate_main_program(self):21         # self._main_process = subprocess.Popen([sys.executable, self._entry_program])22         main_thread = threading.Thread(target = MainProgram.start_program)23         main_thread.setDaemon(True)24         main_thread.start()25         self._start_time = time.time()26 27     def _initiate_checker(self):28         while True:29             try:30                 self._do_check()31             except KeyboardInterrupt:32                 sys.exit(1)33 34     def _do_check(self):35         sys.stdout.flush()36         time.sleep(self._check_interval)37         if self._is_change_running_code():38             print 'The elapsed time: %.3f' % (time.time() - self._start_time)39             # self._main_process.kill()40             # self._main_process.wait()41             sys.exit(5666)42 43     def _is_change_running_code(self):44         for mod_name in sys.modules.iterkeys():45             if mod_name in self._exclude_mod:46                 continue47             cur_mtime = self.get_mtime(mod_name)48             last_mtime = self._mod_mtime.get(mod_name)49             if cur_mtime != self._mod_mtime:50                 # 更新程序运行过程中可能导入的新模块51                 self._mod_mtime.update({mod_name : cur_mtime})52                 if last_mtime and cur_mtime > last_mtime:53                     return True 54         return False55 56     @staticmethod57     def get_mtime( mod_name):58         ld_mod = sys.modules.get(mod_name)59         file = getattr(ld_mod, '__file__', None)60         if file and os.path.isfile(file):61             file = file[:-1] if file[-4:] in ('.pyc', '.pyo') else file62             if file.endswith('.py'):63                 return os.stat(file).st_mtime 64         return None65 66 if __name__ == '__main__':67     print 'Enter entry point...'68     check = Checker()69     check.start()70     print 'Entry Exit!'
View Code

 ./Reloader.py

1 def set_sentry(): 2     while True: 3         print '====== restart main program... =====' 4         sub_process = subprocess.Popen([sys.executable, r'./Entry.py'], 5             stdout = None,    #subprocess.PIPE 6             stderr = subprocess.STDOUT,) 7         exit_code = sub_process.wait() 8         print 'sub_process exit code:', exit_code 9         if exit_code != 5666:10             # 非文件修改导致的程序异常退出,没必要进行重启操作11             print 'main program exit code: %d' % exit_code12             break13 14 if __name__ == '__main__':15     try:16         set_sentry()17     except KeyboardInterrupt:18         sys.exit(1)
View Code

运行Reloader.py,然后在编辑器中修改mainProgram.py,结果如下:

====== restart main program... =====Enter entry point...program is running...MainProgram 0MainProgram 1MainProgram 2MainProgram 3MainProgram 4MainProgram 5The elapsed time: 6.000sub_process exit code: 5666====== restart main program... =====Enter entry point...program is running...MainProgram 0MainProgram 100MainProgram 200MainProgram 300[Cancelled]
View Code

这其中的主要涉及的问题如下:

  • 检测程序和主程序要分别位于不同进程/线程,并且要能共享进程资源;
  • 检测程序主动退出时,执行主程序的线程要关闭掉(注意:python threading没有提供直接kill线程的接口);

以上问题决定了检测程序和主程序要分别以子进程及其创建的线程的方式运行。

上面的程序中并没有通过遍历工程目录的所有文件的改动状况来重启程序,而是只检测已经被加载到内存中的模块,避免修改暂时没有被使用的文件导致错误的重启。

这个例子仅仅是为了展示一种思路,将线程设置为守护线程以强迫其随着创建进程的结束而退出的做法可能导致资源没有正确释放。

但这种方式本质上并不是热更,也没有保留程序的执行状态,可以看做是一个自动化重启的工具。

3.基于函数替换

下面我们从简单到深入一步步的说明函数替换的热更原理。

3.1 __dict__ vs attrs

先来看一个简例:

class Foo(object):    STA_MEM = 'sta_member variable'     @staticmethod    def sta_func():        print 'static_func'     @classmethod    def cls_func(cls):        print 'cls_func'     def func(self):        print "member func"

 下面比较一下上面类中定义的三个函数:

comp = [(Foo.sta_func, Foo.__dict__['sta_func']),(Foo.cls_func, Foo.__dict__['cls_func']),(Foo.func, Foo.__dict__['func'])]for attr_func, dic_func in comp:    for func in (attr_func, dic_func):        print func, type(func), id(func), inspect.ismethod(func), inspect.isfunction(func), isinstance(func, classmethod), isinstance(func, staticmethod)

 看一下比较结果:

                
      40923824  False  True  False  False
               
   40873104  False   False False   True
>    
  40885944 True False False False
               
   40873296 False False True False
                    
 40886024 True False False False
                  
     40926064 False True False False

可以看到Foo.func和Foo.__dict__['func']获取的并不是同一个对象,类型也不同。

简单可以理解为对于类类型,__dict__中包含的是类的namespace。里面是原生的函数定义,而通过点运算符得到的是类的属性。

关于这个详细解释可以参考 和  . 这里不做过多说明。

3.2 运行时替换对象成员函数

为了便于说明如何在程序运行时替换函数,下面刻意设计的一个简单的例子:

 ./hotfix.py

# -*- coding:utf-8 -*-gl_var = 0class Foo(object):    def __init__(self):        self.cur_mod = __name__    def bar(self):        print 'This is Foo member func bar, self.cur_mod = %s' % self.cur_modf = Foo()f.bar()print 'hotfix gl_var = %d\n' % gl_var

 ./reloader.py (只使用reload)

import hotfixif __name__ == '__main__':    foo = hotfix.Foo()    foo.cur_mod = __name__    cmd = 1    while 1 == cmd:        reload(hotfix)        foo.bar()        cmd = input()

运行测试结果:

G:\Cnblogs\Alpha Panda>python Reloader.pyThis is Foo member func bar, self.cur_mod = hotfixhotfix gl_var = 0This is Foo member func bar, self.cur_mod = hotfixhotfix gl_var = 0This is Foo member func bar, self.cur_mod = __main__####### 修改hotfix.Foo.bar函数的定义 #######1After Modified! This is Foo member func bar, self.cur_mod = hotfixhotfix gl_var = 0This is Foo member func bar, self.cur_mod = __main__
View Code

 上面的结果说明修改hotfix.Foo.bar的定义并reload之后,新定义的函数对于新建的对象是生效的,但是对于已经存在的对象reloader.foo并不生效。下面添加函数替换:

1 import hotfix 2  3 def reload_with_func_replace(): 4     old_cls = hotfix.Foo 5     reload(hotfix) 6     for name, value in hotfix.Foo.__dict__.iteritems(): 7         if inspect.isfunction(value) and name not in ('__init__'): 8             # setattr(foo.bar, 'func_code', hotfix.Foo.bar.func_code) 9             old_func = old_cls.__dict__[name]10             setattr(old_func, "func_code", value.func_code)11     setattr(hotfix, 'Foo', old_cls)12 13 if __name__ == '__main__':14     foo = hotfix.Foo()15     foo.cur_mod = __name__16     cmd = 117     while 1 == cmd:18         reload_with_func_replace()19         foo.bar()20         cmd = input()

 看一下测试结果:

G:\Cnblogs\Alpha Panda>python Reloader.pyThis is Foo member func bar, self.cur_mod = hotfixhotfix gl_var = 0This is Foo member func bar, self.cur_mod = hotfixhotfix gl_var = 0This is Foo member func bar, self.cur_mod = __main__1After Modified! This is Foo member func bar, self.cur_mod = hotfixhotfix gl_var = 0After Modified! This is Foo member func bar, self.cur_mod = __main__
View Code

在没有重新创建reloader模块中的对象foo的情况下,被修改后的函数代码被执行了,而且对象的状态(self.cur_mod)被保留下来了。

3.3 函数替换一般化

显然上面的代码只是为了演示,使用reload要事先知道并确定模块,而且只能运用于绑定到模块的变量上,程序运行过程中通过sys.modules拿到的模块都是是str类型的,因此使用runtime使用reload显然不合适。

1 RELOAD_MOD_LIST = ('hotfix',) 2  3 def do_replace_func(new_func, old_func): 4     # 暂时不支持closure的处理 5     re_attrs = ('func_doc', 'func_code', 'func_dict', 'func_defaults') 6     for attr_name in re_attrs: 7         setattr(old_func, attr_name, getattr(new_func, attr_name, None)) 8  9 def update_type(cls_name, old_mod, new_mod, new_cls):10     old_cls = getattr(old_mod, cls_name, None)11     if old_cls:12         for name, new_attr in new_cls.__dict__.iteritems():13             old_attr = old_cls.__dict__.get(name, None)14             if new_attr and not old_attr:15                 setattr(old_cls, name, new_attr)16                 continue17             if inspect.isfunction(new_attr) and inspect.isfunction(old_attr):18                 do_replace_func(new_attr, old_attr)19                 # setattr(old_cls, name, new_attr)20         setattr(new_mod, cls_name, old_cls)21 22 def reload_with_func_replace():23     for mod_name in RELOAD_MOD_LIST:24         old_mod = sys.modules.pop(mod_name)        # Not reload(hotfix)25         __import__(mod_name)                    # Not hotfix = __import__('hotfix')26         new_mod = sys.modules.get(mod_name)27         for name, new_attr in inspect.getmembers(new_mod):28             if new_attr is not type and isinstance(new_attr, type):29                 update_type(name, old_mod, new_mod, new_attr)

上面重写了3.2中的reload_with_func_replace,这样只要在RELOAD_MOD_LIST中指定需要热更的模块或者定义一个忽略热更的列表模块,然后需要的时候触发一个指令调用上面的热更流程,便可实现运行时对sys.modules中部分模块实施热更新。

加上对闭包的处理:

def do_replace_func(new_func, old_func, is_closure = False):    # 简单的closure的处理    re_attrs = ('func_doc', 'func_code', 'func_dict', 'func_defaults')    for attr_name in re_attrs:        setattr(old_func, attr_name, getattr(new_func, attr_name, None))    if not is_closure:        old_cell_nums = len(old_func.func_closure) if old_func.func_closure else 0        new_cell_nums = len(new_func.func_closure) if new_func.func_closure else 0        if new_cell_nums and new_cell_nums == old_cell_nums:            for idx, cell in enumerate(old_func.func_closure):                if inspect.isfunction(cell.cell_contents):                    do_replace_func(new_func.func_closure[idx].cell_contents, cell.cell_contents, True)

上面仅仅对含有闭包的情况进行了简单处理,关于闭包以及cell object相关的介绍可以参考一下我的另一篇博文:.

4.小节

上面完整介绍了基于函数热更的原理以及其核心的地方。考虑到python代码的语法很灵活,要想实际应用于项目中,还有很多要完善的地方。而且热更对运行时代码的更新能力有限,重大的修改还是需要重启程序的。就好比一艘出海的轮船,热更仅仅可以处理一些零件的替换和修复工作,如果有重大的问题,比如船的引擎无法提供动力,那还是要返厂重修才能重新起航的:-)。

限于篇幅先介绍到这里,有问题欢迎一起讨论学习。

转载于:https://www.cnblogs.com/yssjun/p/10182076.html

你可能感兴趣的文章
cmake是什么
查看>>
使用MASM10(变量的使用)- Win32汇编语言018
查看>>
【Docker学习笔记】----基于centos 7 的Docker安装
查看>>
Android笔记之OnLongClickListener
查看>>
Java客户端:调用EyeKey HTTP接口进行人脸对比
查看>>
SQL之分区函数
查看>>
创业公司如何实施敏捷开发
查看>>
Django使用AJAX调用自己写的API接口
查看>>
数据科学求职准备
查看>>
Wireshark抓包工具使用教程以及常用抓包规则
查看>>
fedora16下更改网卡名字
查看>>
awk中NF,NR的含义
查看>>
Centos下Docker中运行neo4j 并配置挂载本地文件
查看>>
静态页面跳转传值小插件
查看>>
JetBrains公司的IDE使用技巧
查看>>
第三届中国云计算用户大会笔记和心得
查看>>
PHP7开启opcache打造强悍性能
查看>>
加载某个页面(A)时实现自动跳转到某个页面(B)
查看>>
Jenkins入门系列之——03PDF文档下载
查看>>
Digit Generator(生成元)
查看>>