首发于 Python杂谈

Python|黑魔法:模块热更新的案例&思路

模块层热更新案例:

上一篇写了代码层的热更新案例:

Python黑魔法:热更新的案例和思路(上)

相对应的这篇模块层更新,即调用内置函数 reload 的方式,更新整个模块,常见于调试代码时,在 IDE 修改代码,再希望通过某种自动化操作触发重载模块更新代码。

若调试时每次都要写一堆动态逻辑代码,还不如直接重启大法,相反,模块的重载带来的不可预知性变化也导致其非特定情况下不会选择用于线上修复。

案例:调试时想让试试猫吃鱼是什么效果,猫吃大象是什么效果,猫吃恐龙是什么效果...同时又不想停止程序

class Cat(object):
	def Eat(self):
		print("Cat eat fish")
cat = Cat()

下例直接使用了 raw_input() 中断程序的时间去改代码, Ctrl+S(很重要) ,再跑 HotFix 函数, 模拟 调试时情景。

import Zoom
def HotFix():
    # 此时程序中断, 将Zoom模块的Eat进行修改, 然后控制台按回车,程序继续
    # =====中断=====
    input()
    # =====中断=====
    import importlib
    importlib.reload(Zoom)
print ("热更新前")
cat = Zoom.cat
cat.Eat()
HotFix()
print ("热更新后")
print ("cat还是Zoom的cat吗", Zoom.cat is cat)
Zoom.cat.Eat()
cat.Eat()
# 打印结果:
# 热更新前
# Cat eat fish
# 回车符
# 热更新后
# cat还是Zoom的cat吗 False
# Cat eat elephant
# Cat eat fish

HotFix 函数使用 reload 重载后, Zoom 的类与全局变量全部易主,旧 Cat类 仍然被旧 cat对象 引用,即旧 Cat类 的对象是没更新的,且全局变化会重新初始化。

引用图解:

reload 前:

reload 后:

这就是 reload 比较让人谈之色变的原因,它仅仅是将一个模块进行重载,用原指针指向 的类,函数等,原模块定义的旧类,旧函数该被谁引用还是被谁引用。 reload 的问题在于:

  • 原全局变量会被覆盖掉,如果该全局变量对象的状态会被重置,导致程序失常。
  • 同样上一篇模块层热更新案例 情景3 遇到的问题,即重载后实例的类仍然是指向旧类。

显而易见的,用 reload 热更新需解决的两个问题:

  • 全局变量如何处理?
  • 旧类的实例对象如何更新?

全局变量处理:

  1. 数据复原方向:
  • 每个模块写一个 OnReload 函数专门处理当模块被重载时被调用。
  • 每个全局对象的类写数据重读接口,每次实例化时调用。
  • ......

看着是规矩工整,需要花费极大的力气多起一层规则约束全部模块达到规范性和普遍性,目的仅仅是调试而言,实在有点小题大做了。

2. 引用复原方向

将重载前模块的全局变量替换重载后的全局变量,考虑到实际调试极情况下极大部分修改是希望修改部分宏定义,函数,类函数,这个方向是相对合理的。

def HotFix():
    reload模块
    :return:
    # 中断 修改代码
    input()
    import importlib
    from types import FunctionType
    oldMoudle = __import__("Zoom")
    oldMoudleData = {}
    attrList = dir(oldMoudle)
    # 将旧模块的东西全部保存
    for attrName in attrList:
	oldMoudleData[attrName] = getattr(oldMoudle, attrName)
	# 重载模块
	importlib.reload(oldMoudle)
	newMoudle = __import__("Zoom")
	for attrName in dir(newMoudle):
	    if attrName in oldMoudleData:
		if isinstance(oldMoudleData[attrName], type) \
			or isinstance(oldMoudleData[attrName], FunctionType) \
			or isinstance(oldMoudleData[attrName], int) \
			or isinstance(oldMoudleData[attrName], float) \
			or isinstance(oldMoudleData[attrName], str):
		else:
		    setattr(newMoudle, attrName, oldMoudleData[attrName])
import Zoom
print("热更新前")
cat = Zoom.cat
cat.Eat()
HotFix()
print("热更新后")
print("cat还是Zoom的cat吗", Zoom.cat is cat)
Zoom.cat.Eat()
cat.Eat()
# 打印结果:
# 热更新前
# Cat eat fish
# 热更新后
# cat还是Zoom的cat吗 True
# Cat eat fish
# Cat eat fish

上例在 reload 前先用字典 oldMoudleData 将模块重载前的一切属性保存起来(包括旧模块的类,全局函数等,通过 dir getattr 获取)。

但存在了一个矛盾,希望全局变量的引用复原,同时又希望同样为全局变量的宏定义不复原,于是引用复原时过滤掉类型为整数,浮点数,字符串的对象,当然,这是较自定义的选择。

更新旧类实例对象:

三种方式:

  • 重新生成实例对象
  • 切换实例对象的类
  • 对实例对象的类更新

1.重新生成实例对象

直接简单的操作使当前对象销毁并重新生成,如刷新界面等,新的实例的类会是 reload 后模块的类,例如刷新界面等操作。

2.切换实例对象的类

reload 模块后,通过代码层动态语句运行指定对象的 __class__ 属性替换为 reload 后模块的新类

import Zoom
print("热更新前")
cat = Zoom.cat
cat.Eat()
HotFix()
cat.__class__ = Zoom.Cat
print("热更新后")
cat.Eat()
# 打印结果:
# 热更新前
# Cat eat fish
# 热更新后
# Cat eat elephant

这种方式能达成目的,但前提条件是:

  • 要额外写代码,这不是纯粹的模块层更新了
  • 能找到所有该类的对象

这有违方便调试的初衷,应用场景回到了线上 BUG 修复而不是调试,这又于线上 BUG 修复需要可预知和稳定相违背,用武之地较少。

3.对实例对象的类更新:

不改变实例对象对旧类的引用,将旧类的类函数等引用修改指向新类的类函数等,并在重载后的模块取代新类,间接完成热更新。

def HotFix():
    reload模块
    :return:
    input()
    import importlib
    from types import FunctionType
    oldMoudle = __import__("Zoom")
    oldMoudleData = {}
    attrList = dir(oldMoudle)
    # 将旧模块的东西全部保存
    for attrName in attrList:
    	oldMoudleData[attrName] = getattr(oldMoudle, attrName)
	# 重载模块
	importlib.reload(oldMoudle)
	newMoudle = __import__("Zoom")
	for attrName in dir(newMoudle):
	    if attrName in oldMoudleData:
		if isinstance(oldMoudleData[attrName], FunctionType) \
		        or isinstance(oldMoudleData[attrName], int) \
		        or isinstance(oldMoudleData[attrName], float) \
		        or isinstance(oldMoudleData[attrName], str):
	    elif isinstance(oldMoudleData[attrName], type):
		ReplaceClassFunc(getattr(newMoudle, attrName), oldMoudleData[attrName])
		setattr(newMoudle, attrName, oldMoudleData[attrName])
	    else:
		setattr(newMoudle, attrName, oldMoudleData[attrName])
def ReplaceClassFunc(srcClass, desClass):
    for attrName in dir(srcClass):
	attr = getattr(srcClass, attrName)
	from types import FunctionType
	if isinstance(attr, FunctionType) \
	    or isinstance(attr, int) \
	    or isinstance(attr, float) \
            or isinstance(attr, str):
	    setattr(desClass, attrName, attr)
import Zoom
print("热更新前")
cat = Zoom.cat
cat.Eat()
HotFix()
print("热更新后")
cat.Eat()