# reload实现代码热更新

简而言之:没啥大用,了解就好-多的是from a import b/*的情况

# reload

reload 用于重新加载模块。与 Python 2 时代不同,reload 在最新的 Python 中不再作为内建函数存在了,而被移入标准库 importlib 模块中。

假设我们有一个配置模块 config.py ,以变量形式定义着一些配置项:

wx = 'coding-fan'
title = '10086'

在程序中,我们只需将 config 模块导入,即可访问里面定义的每个配置项:

>>> import config
>>> print(config.title)
10086

现在,我们编辑 config.py 文件,将配置进行调整:

wx = 'fasionchan'
title = 'Python开发工程师'

如不做任何处理,程序无法获得调整后的配置,这一点都不意外:

>>> print(config.title)
10086

想要获取最新的配置,我们只能让 Python 重新加载 config ,调用 reload 函数即可:

>>> import importlib
>>> importlib.reload(config)
<module 'config' from '/Users/fasion/config.py'>

重新加载 config 模块后,我们成功获得最新配置,而程序完全无须重启!

>>> print(config.title)
Python开发工程师

借助 reload 函数,我们成功实现了一定程度的 代码热更新 能力!

利用操作系统文件事件通知机制,我们还可以让 Python 在代码文件发生变化时自动加载新代码!已经有先行者为此开发了 watchdog 包。下面是一个示例:

import importlib
import time
import os.path

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler, LoggingEventHandler

import config

class CodeEventHandler(FileSystemEventHandler):

    def on_modified(self, event):
        if event.src_path == config.__file__:
            print('reloading config')
            importlib.reload(config)

def main():
    observer = Observer()
    observer.schedule(CodeEventHandler(), os.path.dirname(config.__file__), True)
    observer.start()

    while True:
        print('title:', config.title)
        time.sleep(5)

if __name__ == '__main__':
    main()

这个例子先实现文件事件处理类 CodeEventHandler ,on_modified 方法接收修改事件。如果被修改文件刚好是 config 模块源码文件,我们调用 importlib.reload 重新加载 config 模块。

接着在 main 函数,我们先初始化 watchdog 观测器 Observer ,然后启动它。这样一来,只要被观测路径上发生修改事件,watchdog 将调用 CodeEventHandler 对象的 on_modified 。

注意到,我们不是直接观测 config.py 文件,而是递归观测其所在目录。如果直接观测目标文件,当它被删掉重建,便失去跟踪;相反,观测目标文件所在目录,重新创建的新文件也会得到跟踪。

最后,程序进入主题逻辑,例子用一个周期性输出配置值 title 的循环来充当这个角色。

把这个程序跑起来后,它将不断输出 config.title 的值;当 config.py 被修改后,它将输出 reloading config ,并重新加载 config 模块。

现在,试着修改 config.py 中的 title 变量,程序将自动生效,无须重启:

title: Python开发工程师
reloading config
title: 10086

虽然例子中只涉及到一些简单的变量,但这种机制对诸如 函数 以及 等复杂对象也是支持的。接下来,我们将更进一步,充分认识 reload 机制的局限性,并探索破解局限性的方案。

# reload局限性

# 重新执行而非覆盖

通过 reload 函数重新加载模块,Python 将以原模块对象属性空间为全局/局部名字空间,再次执行模块代码。这种行为将导致一些诡异的现象:

首先,旧模块变量不会被删除,除非在新模块代码中显式删除,这很好理解。

假设模块 mo.py 原来有两个变量 ab

a = 1
b = 2

模块导入后删掉 a 、修改 b 并新增 c :

b = 22
c = 3

Python 重新加载模块 mo 时,以原模块对象属性空间为局部名字空间,执行新的模块代码。模块代码对 b 和 c 进行赋值,这样变引入的新变量 c ,变量 a 却被遗忘了,继续残留在模块中。关于模块加载以及模块代码执行过程,请参考虚拟机部分相关章节。

不过,就算模块旧变量不删,最多也就是不够严谨而已,对于实现代码更新影响不大。

# 对已经暴露到外部的无能为力

reload 只会更新模块属性空间,对已暴露到外部的却无能为力。

>>> from mo import b
>>> print(b)
2

这段代码将 b 引入到当前局部名字空间,就不受模块 mo 约束了。当我们修改模块并重新加载后,b 保持不变:

>>> importlib.reload(mo)
>>> print(b)
2

图片描述

如上图,我们可以清楚地看到,经过 reload 后,模块中的 b 得到了更新,但当前局部名字空间中的 b 保持不变。如果通过 mo 模块间接访问变量 b ,更新是可见的:

>>> print(mo.b)
22

因此,想要通过 reload 实现热更新,最好通过模块对象引用模块属性,不要直接导入。

# 被依赖的模块不会自动reload

如果在模块 mo 中 import 其他模块,reload 模块 mo 时,其他模块不会 reload 。这是要么显式 reload 其他模块,还要特别注意顺序;要么将其他模块路径从 sys.modules 中剔除,之后 Python 将全新加载它。

可以将 sys.modules 理解成模块对象运行时缓存,Python 导入一个模块后,将以模块路径为 key ,以模块对象为 value 保存在该字典中。当同一模块被二次 import 时,Python 直接从这取出已加载的模块对象,避免重复加载。

因此,当我们将某个模块从 sys.modules 中剔除,Python 将创建新的模块对象并执行模块代码。