python 模块热更新
士多碧莉 人气:0最近有个部署需求,需要读取py文件格式的配置项,我的实现思路是把配置文件解析到内存中。主要使用两种方法:
- importlib.import_module
- types.ModuleType
方法1、使用 import_module 动态导包
先来看看import module使用方法。
- 主要有两个参数:
- package:包名
- name:模块名
- 返回 module 对象
现在开始实现动态导包,成功读取到配置项。
import importlib settings = importlib.import_module("remote_settings")
这样子就能初步实现动态倒入了,但是我有个需求,就是我的系统好些个模块,用FOR循环导包,然后处理业务。然后问题来了,对同一个“包”导入多次,python并不会重新导入,而是返回内存缓存中该模块的地址。
下面验证一下,第一次写入a = 123,第二次写入a = "hello"。
输出结果,两次都是打印旧版本的变量,可见对同一个模块进行多次import_module,并不能实现热更新。
必须要reload,模块才会更新。
输出结果如下,动态reload后,成功获得新版本a的值。
到此基本实现初步热更新需求了,但是还有个问题:
问题一:重新加载的模块不删除旧版本在符号表中的登记项,比如旧版本中存在变量a,新版本中删除了该变量,但是重载不会更新该变化。
def load_module(module_name): module = importlib.import_module(module_name) return importlib.reload(module) def rewrite_file(file_name, content): with open(file_name, "w+") as f: f.write(content) def main(): rewrite_file(file_name, "a=123\nb=456") c1 = load_module(module_name) print(hasattr(c1, "a")) rewrite_file(file_name, "c=100\nd=200") c1 = load_module(module_name) print(hasattr(c1, "a"))
我们期望输出 True、False,但是两次都是输出True,也就是说重新加载的模块不会删除最初旧版本模块在符号表中的登记项。
方法2、使用types.ModuleType 创建模块对象
手动创建module对象,而不是使用内存中的module对象。这种方法不需要判断是否需要重载,而且是真正的更新,会删除旧版本模块的登记项。
import types def import_from_pyfile(filename): d = types.ModuleType("config") # 创建一个模块对象 d.__file__ = filename try: with open(filename, "r") as config_file: exec(compile(config_file.read(), filename, "exec"), d.__dict__) except ImportError as e: print("failt to read config file: {}".format(filename)) raise e return d
下面验证一下
我们期望的输出依次是True、False,符合需求
因此,这种方法能让我们的模块实现真正的重载。
一些注意事项
无论是方法1还是方法2,都是返回一个module对象,module对象存在一些共性问题。
问题一:重新加载类不影响类的任何已存实例,已存实例将继续使用原来的定义,只有重新加载后创建的新实例使用新定义。
# 原先的 Dog 定义 # class Dog(): # def __init__(self): # self.name = None c1 = load_module(module_name) old_dog = c1.Dog() # 中间去修改了 Dog 定义 # class Dog(): # def __init__(self): # self.name = "旺财" c1 = load_module(module_name) new_dog = c1.Dog() print(old_dog.name, new_dog.name) >>> ouput: None 旺财
问题二:模块内的引用,不会被reload。比如模块configA中引用了其他模块(configB),当configB发生变化,重新加载configA,并不会对configB进行重载。
预期应该依次输出 configB version1、configBversion2,但是输出了两次configB version1,这说明了模块内的引用,不会被reload,需要手动更新它。
我这实现了一个递归更新方法,不仅对当前模块热更新,还更新里面所有的引用。
def load_module(module): if isinstance(module, str): # 首次import module = importlib.import_module(module) return importlib.reload(module) def reload_module(module): load_module(module) for key, child_module in vars(module).items(): if isinstance(child_module, types.ModuleType): reload_module(child_module)
效果如下:
def test_reload_module(): configA = "config" configB = "./configB.py" configC = "./configC.py" rewrite_file(configB, "import configC\nname ='configB version1'") rewrite_file(configC, "name ='configC version1'") confA = load_module(configA) print("原始configB.name:", confA.configB.name) print("原始configC.name:", confA.configB.configC.name) a = 123 rewrite_file(configB, "import configC\nname ='configB version2'") rewrite_file(configC, "name ='configC version2'") confA = load_module(configA) print("非递归重载configA, configB.name:", confA.configB.name) print("非递归重载configA, configC.name:", confA.configB.configC.name) reload_module(confA) print("递归重载configA, configB.name:", confA.configB.name) print("递归重载configA, configC.name:", confA.configB.configC.name)
日志如下:
加载全部内容