python基于函数替换的热更新原理介绍

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

1.基于reload

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

先来看一下下面的问题:

>>> import sys, math
>>> reload(math)
<module ‘math‘ (built-in)>
>>> sys.modules.pop(‘math‘)
<module ‘math‘ (built-in)>
>>> __import__(‘math‘)
<module ‘math‘ (built-in)>
>>> reload(math)
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    reload(math)
ImportError: reload(): module math not in sys.modules
>>> sys.modules.get(‘math‘)
<module ‘math‘ (built-in)>
>>> 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, sys
import hotfix
# r = hotfix.gl_var

# @singleton
class 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 None

if __name__ == ‘__main__‘:
    reload_mgr = ReloadMgr()
    reload_mgr.polling()

上面的这个例子轮询检测已经被导入过的指定模块的源代码是否被修改过,如果被修改过,使用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__, cnt
10     cnt += 1
11
12 def start_main_loop():
13     frame_time = 1
14     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()

./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 = None
10         self._check_interval = 1.0
11         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 = 0
15
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                 continue
47             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 False
55
56     @staticmethod
57     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 file
62             if file.endswith(‘.py‘):
63                 return os.stat(file).st_mtime
64         return None
65
66 if __name__ == ‘__main__‘:
67     print ‘Enter entry point...‘
68     check = Checker()
69     check.start()
70     print ‘Entry Exit!‘

./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_code
12             break
13
14 if __name__ == ‘__main__‘:
15     try:
16         set_sentry()
17     except KeyboardInterrupt:
18         sys.exit(1)

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

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

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

  • 检测程序和主程序要分别位于不同进程/线程,并且要能共享进程资源;
  • 检测程序主动退出时,执行主程序的线程要关闭掉(注意: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)

看一下比较结果:

<function sta_func at 0x027072B0>                 <type ‘function‘>      40923824  False  True  False  False
<staticmethod object at 0x026FAC90>               <type ‘staticmethod‘>    40873104  False   False  False   True

<bound method type.cls_func of <class ‘__main__.Foo‘>>    <type ‘instancemethod‘>   40885944 True False False False
<classmethod object at 0x026FAD50>                <type ‘classmethod‘>     40873296 False False True False

<unbound method Foo.func>                      <type ‘instancemethod‘>  40886024 True False False False
<function func at 0x02707B70>                   <type ‘function‘>      40926064 False True False False

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

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

关于这个详细解释可以参考instancemethod or function 和 from function to method . 这里不做过多说明。

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

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

./hotfix.py

# -*- coding:utf-8 -*-
gl_var = 0

class 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_mod

f = Foo()
f.bar()
print ‘hotfix gl_var = %d\n‘ % gl_var

./reloader.py (只使用reload)

import hotfix

if __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.py
This is Foo member func bar, self.cur_mod = hotfix
hotfix gl_var = 0

This is Foo member func bar, self.cur_mod = hotfix
hotfix gl_var = 0

This is Foo member func bar, self.cur_mod = __main__
####### 修改hotfix.Foo.bar函数的定义 #######
1
After Modified! This is Foo member func bar, self.cur_mod = hotfix
hotfix gl_var = 0

This is Foo member func bar, self.cur_mod = __main__

上面的结果说明修改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 = 1
17     while 1 == cmd:
18         reload_with_func_replace()
19         foo.bar()
20         cmd = input()

看一下测试结果:

G:\Cnblogs\Alpha Panda>python Reloader.py
This is Foo member func bar, self.cur_mod = hotfix
hotfix gl_var = 0

This is Foo member func bar, self.cur_mod = hotfix
hotfix gl_var = 0

This is Foo member func bar, self.cur_mod = __main__
1
After Modified! This is Foo member func bar, self.cur_mod = hotfix
hotfix gl_var = 0

After Modified! This is Foo member func bar, self.cur_mod = __main__

在没有重新创建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                 continue
17             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相关的介绍可以参考一下我的另一篇博文:理解Python闭包概念.

4.小节

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

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

原文地址:https://www.cnblogs.com/yssjun/p/10182076.html

时间: 2024-11-05 16:03:45

python基于函数替换的热更新原理介绍的相关文章

ios WaxPatch热更新原理

以下是引用他人文章内容: 为什么需要 WaxPatch 很多情况下,已经在 AppStore 上线的应用需要紧急缺陷修复,此时便需要使用某些技术手段,使应用程序能够动态下载补丁,进行缺陷修复. 什么是 WaxPatch 迄今为止,脚本语言中运行速度最快的是 Lua.Lua 语言由巴西里约热内卢天主教大学的 Roberto Ierusalimschy.Waldemar Celes 和 Luiz Henrique de Figueiredo 于 1993 年开发的.其最初的设计目的是提供一个方便嵌入

Webpack热更新原理

开发环境页面热更新早已是主流,常见的需求如赛事网页推送比赛结果.网页实时展示投票或点赞数据.在线评论或弹幕.在线聊天室等,都需要借助热更新功能,才能达到实时的端对端的极致体验. webpack-hot-middleware webpack-hot-middleware中间件是webpack的一个plugin,通常结合webpack-dev-middleware一起使用.借助它可以实现浏览器的无刷新更新(热更新),即webpack里的HMR(Hot Module Replacement).如何配置

webpack基本概念、打包流程和热更新原理

webpack基本概念 1. webpack中的module,chunk 和 bundle module 就是一个js模块,就是被require或export的模块,例如 ES模块,commonjs模块,AMD模块chunk 是 代码块,是进行依赖分析的时候,代码分割出来的代码块,包括一个或多个module,是被分组了的module集合,code spliting之后的就是chunkbundle 是 文件,最终打包出来的文件,通常一个bundle对应一个chunk 2. webpack中load

python replace函数替换无效问题

str = "hello,china!" str.replace("hell","well") print(str) 写代码时发现这样替换之后并没有替换成功. 原因: 在Python中字符串是是不可变对象. 所以字符串使用replace需要重新赋值,生成一个新的对象. 之前没有重新引用,导致该变量 指向的是 以前的对象,实则已经发生变化,只是没有重新引用而已. 所以要想打印出替换后的字符串需要重新赋值,如下: str = "hello,

cmd命令查看Python模块函数等帮助文档和介绍

dir函数式可以查看对象的属性 使用方法很简单,举os类型为例,在Python命令窗口输入 dir(‘os’) 即可查看os模块的属性 打开cmd命令窗口 ? 输入python(注意:计算机需要有Python环境,配置好Python环境变量) ? 输入dir('os')命令 ? 如何查看对象某个属性的帮助文档 ? 如要查看’os’的split属性,可以用__doc__, 使用方法为print(’os’.split.__doc__) print(’os’.split.__doc__) ? 查看对象

vue 无法热替换/热更新

参考了很多小伙伴的解决办法: 1.https://blog.csdn.net/win7583362/article/details/65443291 2.https://www.jianshu.com/p/23d705829d3b 虽然我并不是用对方的方法解决的,也不是如2)提及的 对文件夹或者文件名称大小写敏感引起的. 依然还是建议大家打开学习一下,因为我是比较粗心的人,??的小伙伴文章所讲的 也许会对你在某些点(我没有注意到的地方)有助益. 热替换/热更新原理??:(引用1) 讲解一下热替换

cocos2dx 3.1.1 在线热更新 自动更新(使用AssetsManager更新游戏资源包)

为什么要在线更新资源和脚本文件? 简单概括,如果你的游戏项目已经在google play 或Apple Store 等平台上架了,那么当你项目需要做一些活动或者修改前端的一些代码等那么你需要重新提交一个新版本给平台.但是平台审核和具体的上架时间是个不确定的.具体什么时候能上架,主要由具体的平台决定. 如果游戏项目是使用脚本语言进行编写的(如lua.js),那么一旦需要更新,则可以通过从服务器下载最新的脚本和资源,从而跳过平台直接实现在线更新.(有些平台是禁止在线更新资源方式的,但是你懂得) 闲话

再说说erlang的模块热更新

前面的文章有讲过erlang热更新,只是大概介绍,现在再深入一点讲erlang的模块热更新.erlang的热更新是模块级别的,就是一个模块一个模块更新的. 热更新是什么,就是在不停止系统的情况下对运行的代码进行替换. 如何进行热更新? c(Mod) -> compile:file(Mod), code:purge(Mod), code:load_file(Mod). 以上就是shell c(Mod) 的主要代码,3个步骤:编译新的代码,清除旧代码,加载新代码 同样, l(Mod) 的主要代码如下

Cordova 代码热更新 - 简书

原文:Cordova 代码热更新 - 简书 Cordova 代码热更新 [图片上传失败...(image-a19be7-1521624289049)] 基于 Cordova 框架能将网页应用 (js, html, css, 图片等) 打包成 App.当 App 在应用商店上架后,如何快速更新是我们需要考虑的问题.?? 本地打包新版本 App 发布到应用商店,但这中发布流程耗费时间,尤其是 Apple Store 应用加载网络资源,这样 App 展示的内容就可以保证是最新的,但当应用断网时,应用就