reload函数
原生imp.relaod函数:
- 模块代码将重新编译,模块级别的代码被重新执行,init函数将不再次执行
- 在Python中一切皆为对象,包括函数、变量等,模块中的函数、变量等全部包含在模块字典中,即module_dict,reload方法执行后module_dict中的keys对应的values为新的对象。
- 所有reload之前旧的对象在引用计数为0时被回收。
这就意味着imp.reload在使用过程中有很多问题,比如:
- 不支持from xxx import xxx形式导入的重新加载
- 只支持拓展模块的重新加载,sys、__main__等内置模块不能使用。我们最好不要重新加载除拓展模块外的其他模块。
- reload某个模块,该模块的依赖模块不能自动reload,且只能先reload依赖的模块
- 所有引用旧的对象的变量不会更换引用新的对象。重新加载类,其已实例化对象引用的方法不会变化,继续使用旧类的定义,对于派生类的对象同样如此。
reloader模块
这个库为Python实现了一个基于依赖关系的模块重载器。不像前面imp.reload函数,此重载程序将重新加载请求的模块和依赖于该模块的所有其他模块。对reload函数的问题进行了一定程度上的改进。
需要注意的是,reloader最终还是会调用imp.reload函数来进行reload。
主要分为三个重要步骤:
- 记录模块之间的依赖
- 重新加载模块
- 自定义重新重新加载函数
reloader模块提供了enable、disable、get_dependencies、reload四个接口,应用时调用enable就开始记录模块之间的依赖,使用reload便开始按依赖顺序加载模块。
记录依赖
通过建立模块之间的加载依赖图,可以实现正确的重新加载顺序。
实现的逻辑是通过重载全局的imprt hook函数(__import__)来实现。当需要热更新时,保存原始的__import__函数。当热更新时,我们重新import的行为,记为函数_import,递归调用_import导入模块并将此模块加入其父模块的依赖列表,即可构建模块依赖图。热更新后,恢复原有的__import__函数。
实例代码如下:
import builtins
_baseimport = builtins.__import__
_dependencies = dict()
_parent = None
def _import(name, globals=None, locals=None, fromlist=None, level=-1):
# Track our current parent module. This is used to find our current
# place in the dependency graph.
global _parent
parent = _parent
_parent = name
# Perform the actual import using the base import function.
m = _baseimport(name, globals, locals, fromlist, level)
# If we have a parent (i.e. this is a nested import) and this is a
# reloadable (source-based) module, we append ourself to our parent's
# dependency list.
if parent is not None and hasattr(m, '__file__'):
l = _dependencies.setdefault(parent, [])
l.append(m)
# Lastly, we always restore our global _parent pointer.
_parent = parent
return m
builtins.__import__ = _import
重载模块
递归遍历需要重新加载的模块,这是一个典型的递归调用,visited集合是为了重复导入模块。示例代码如下:
import imp
def _reload(m, visited):
"""Internal module reloading routine."""
name = m.__name__
# Start by adding this module to our set of visited modules. We use
# this set to avoid running into infinite recursion while walking the
# module dependency graph.
visited.add(m)
# Start by reloading all of our dependencies in reverse order. Note
# that we recursively call ourself to perform the nested reloads.
deps = _dependencies.get(name, None)
if deps is not None:
for dep in reversed(deps):
if dep not in visited:
_reload(dep, visited)
# Clear this module's list of dependencies. Some import statements
# may have been removed. We'll rebuild the dependency list as part
# of the reload operation below.
try:
del _dependencies[name]
except KeyError:
pass
# Because we're triggering a reload and not an import, the module
# itself won't run through our _import hook. In order for this
# module's dependencies (which will pass through the _import hook) to
# be associated with this module, we need to set our parent pointer
# beforehand.
global _parent
_parent = name
# Perform the reload operation.
imp.reload(m)
# Reset our parent pointer.
_parent = None
def reload(m):
"""Reload an existing module.
Any known dependencies of the module will also be reloaded."""
_reload(m, set())
自定义__reload__函数
我们可以在模块中自定义__reload__,用于进行数据处理。
def _deepcopy_module_dict(m):
"""Make a deep copy of a module's dictionary."""
import copy
# We can't deepcopy() everything in the module's dictionary because
# some items, such as '__builtins__', aren't deepcopy()-able.
# To work around that, we start by making a shallow copy of the
# dictionary, giving us a way to remove keys before performing the
# deep copy.
d = vars(m).copy()
del d['__builtins__']
return copy.deepcopy(d)
# If the module has a __reload__(d) function, we'll call it with a
# copy of the original module's dictionary after it's been reloaded.
callback = getattr(m, '__reload__', None)
if callback is not None:
d = _deepcopy_module_dict(m)
imp.reload(m)
callback(d)
else:
imp.reload(m)
reloader黑名单
可以使用blacklist参数来设置reload的黑名单:
reloader.enable(blacklist=['os', 'ConfigParser'])
reloader的问题
只解决了imp.reload的模块依赖顺序问题,但是问题1/2/4依旧未解决。在需要reload的模块中自定义__reload__虽然可以解决问题4,但是自己写__reload__显然比较麻烦且容易出错,于是有了下面优化方案。
热更新优化
函数和方法的更新
该方案可以解决imp.reload的问题3和问题4,这种方案本质是对reload后对老代码的模块/对象引用的方法更新到新代码,而变量的值不变。
需要到两个知识点:
- 一个是使用到Python探针技术的
sys.meta_path
,使得可以在import时自定义做点什么事 - 第二个是使用到了
imp.load_module(name, file, pathname, description)
,加载模块, 如果模块已经被加载, 等同于reload()。
单个函数更新逻辑:把老的function的func_code、func_doc、func_dict、func_defaults引用新function的属性
更新流程:
- 先对sys.modules遍历,找到有代码改动或者新增的modules
- 对modules进行import,然后通过
sys.meta_path
机制,对每个module进行后面操作 - 如果是新加的module,直接return,否则进行后面操作
- 通过
inspect.getmembers(module)
找到module所有的function和type类型的对象。 - 对于module的function类型对象,如果有新增function就直接add到老模块中,对已有的function,使用函数更新逻辑更新。
- 对于module的type类型的,对type对象引用的function进行增删改,变量不变。
优点:
- 无论这些function/class以什么方式引用,只要不深入直接引用到func_code/func_default对象,均可动态更新到;
- 有代码改动的模块都会更新一遍,不用考虑模块依赖问题。
缺点:
- 不能动态更新class的派生关系相关的信息
新对象替换旧对象
module被热更新后,找出所有对module中的class/function...有引用的对象,逐个执行新对象替换旧对象的操作。
这个方案实现起来相对简单,但是运行效率过低,一般不做考虑。
其它
- 在热更新期间可以暂停gc
- 配表数据就不仅仅是更新函数,还要更新数据
虽然使用函数和方法的更新的热更新方法基本能满足需求,但是reload终究会让系统卡一段时间,还有优化方案是不要代码自动判断需要更新哪些模块,而是通过指令来指定。但是哪怕这样也还是会有一小段卡顿时间,后面会讲一下另一个热更新方案,可以解决卡顿问题。
引用
Python imp.reload
pypi/reloader
Reloading Python Modules
Python探针技术
python之imp模块