熱更新即在不重啓進程或者不離開Python interpreter的狀況下使得被編輯以後的python源碼可以直接生效並按照預期被執行新代碼。日常開發中,熱更能極大提升程序開發和調試的效率,在修復線上bug中更是扮演重要的角色。可是要想實現一個理想可靠的熱更模塊又很是的困難。html
reload做爲python官方提供的module更新方式,有必定做用,可是很大程度上並不能知足熱更的需求。python
先來看一下下面的問題:shell
>>> 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導入一個模塊分兩步:緩存
- find a module, and initialize it if necessary;
- 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要求模塊以前已經被正確的引入。ide
# -*- 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更新模塊。這種方式思路清晰,實現簡單,然而並無太大的實際用途。主要緣由以下:函數
所以,本質上這個程序僅僅是用做檢測文件修改並使用reload更新,根本的缺陷是舊的對象不能執行新的代碼,須要從新生成新的對象。能夠應用於特定少許文件的更新。工具
針對上面介紹的一個例子存在的問題,可使用進程或者線程將模塊修改檢測的工做和程序的執行分離開來。oop
大體思路就是,不直接啓動主程序,而是啓動一個檢測程序,在檢測程序中建立一個進程或者線程來執行主程序。
./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]
這其中的主要涉及的問題以下:
以上問題決定了檢測程序和主程序要分別以子進程及其建立的線程的方式運行。
上面的程序中並無經過遍歷工程目錄的全部文件的改動情況來重啓程序,而是隻檢測已經被加載到內存中的模塊,避免修改暫時沒有被使用的文件致使錯誤的重啓。
這個例子僅僅是爲了展現一種思路,將線程設置爲守護線程以強迫其隨着建立進程的結束而退出的作法可能致使資源沒有正確釋放。
但這種方式本質上並非熱更,也沒有保留程序的執行狀態,能夠看作是一個自動化重啓的工具。
下面咱們從簡單到深刻一步步的說明函數替換的熱更原理。
先來看一個簡例:
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 . 這裏不作過多說明。
爲了便於說明如何在程序運行時替換函數,下面刻意設計的一個簡單的例子:
./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)被保留下來了。
顯然上面的代碼只是爲了演示,使用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閉包概念.
上面完整介紹了基於函數熱更的原理以及其核心的地方。考慮到python代碼的語法很靈活,要想實際應用於項目中,還有不少要完善的地方。並且熱更對運行時代碼的更新能力有限,重大的修改仍是須要重啓程序的。就比如一艘出海的輪船,熱更僅僅能夠處理一些零件的替換和修復工做,若是有重大的問題,好比船的引擎沒法提供動力,那仍是要返廠重修才能從新起航的:-)。
限於篇幅先介紹到這裏,有問題歡迎一塊兒討論學習。