开源项目源码解读—hotreload,热加载python脚本

1. hotreload

hotreload可以监测python脚本的源码是否发生变动,如果确认源码发生改变,hotreload模块可以重新加载脚本并执行脚本。

该项目git地址: https://github.com/say4n/hotreload ,阅读本文,你将有如下收获:

  1. 了解如何重新加载python脚本
  2. 掌握如何使用多线程技术
  3. 掌握如何判断一个文件的内容是否发生改变

假设你有一个名为script.py的脚本,下面的代码将会检测script.py的源码是否发生修改

import time
import logging
from hotreload import Loader


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    script = Loader("script.py")

    while True:
        # Check if script has been modified since last poll.
        if script.has_changed():
            # Execute a function from script if it has been modified.
            script.main()

        time.sleep(1)

这是作者提供的示例代码,从代码上看,hotreload使用起来很简单,它只能监测一个脚本的源码是否发生改变。

2. hotreload源码解析

源码只有60多行,因此,我将全部代码贴出来

import os
import importlib
import time
import hashlib
import threading
import logging

logger = logging.getLogger("hot_reload")

class Monitor(threading.Thread):
    def __init__(self, loader, frequency = 1):
        super().__init__()

        self.frequency = frequency
        self.loader = loader

        self.daemon = True

    def run(self):
        while True:
            with open(self.loader.source) as file:
                fingerprint = hashlib.sha1(file.read().encode('utf-8')).hexdigest()

            if not fingerprint == self.loader.fingerprint:
                self.loader.notify(fingerprint)

            time.sleep(self.frequency)


class Loader:
    def __init__(self, source):
        self.source = source
        self.__name = os.path.splitext(self.source)[0]
        self.module = importlib.import_module(self.__name)
        self.fingerprint = None

        self.changed = False

        monitor = Monitor(self)
        monitor.start()

    def notify(self, fingerprint):
        self.fingerprint = fingerprint
        try:
            logger.info(f"Fingerprint changed to {fingerprint[:7]}, reloading.")
            self.module = importlib.reload(self.module)
            self.changed = True
        except Exception as e:
            logger.error(f"Reload failed. {e}")

    def has_changed(self):
        logger.info(f"Loader.has_changed called, self.changed is {self.changed}")
        if self.changed:
            self.changed = False
            return True
        else:
            return False

    def __getattr__(self, attr):
        return getattr(self.module, attr)

2.1 Monitor

类 Monitor 负责监测脚本文件的内容是否发生改变

class Monitor(threading.Thread):
    def __init__(self, loader, frequency = 1):
        super().__init__()

Monitor继承自threading.Thread, 这也是一种启动多线程的方式,Monitor还需要实现run方法,想要启动多线程时,可以创建Monitor的实例对象,然后调用start方法, 在类Loader中,就是这样做的。

monitor = Monitor(self)
monitor.start()

Monitor是如何实现监控的呢? 通过文件的指纹,在run方法里,将被监测的文件打开,根据其内容生成一个指纹

with open(self.loader.source) as file:
    fingerprint = hashlib.sha1(file.read().encode('utf-8')).hexdigest()

只要文件的内容发生一丁点的改变,生成的指纹都会改变,用最新的指纹与Loader实例的指纹进行对比,如果不同,说明文件已经发生修改,则调用Loader实例的notify方法告知文件已经发生变化。

if not fingerprint == self.loader.fingerprint:
    self.loader.notify(fingerprint)

2.2 Loader

2.2.1 importlib

类Loader 负责初次加载和重新加载,初次加载时,使用importlib.import_module函数,重新加载时,使用importlib.reload函数。

importlib.import_module接受一个字符串,类似于 xxx.xxx.xxx的格式,在作者的示例中, Loader的初始化参数传入的是script.py,因此在初始化函数中,去掉了文件的后缀

self.__name = os.path.splitext(self.source)[0]

同时,script.py与作者所提供的示例代码是在同一个文件目录下,因此没有采用xxx.xxx.xxx的格式,因为不需要指明模块层级。

但如果script.py 放在了./run 目录下,那么就需要传入run.script.py,import_module才能正确加载。

importlib.reload就简单了,不需要传入字符串,只需要将需要重新加载的模块传入其中就可以了。

2.2.2 __getattr__

这里有必要重点解释一下__getattr__方法,我们先定义一个类

class Demo():
    def __init__(self, name):
        self.name = name

demo = Demo('实例')
print(demo.name)        # 实例
print(demo.age)         # 报错
# demo.show()             # 报错

实例没有age属性,因此最后一行代码会报错,如果调用不存在的show方法,也会报错,然而在作者的示例代码中有一行代码

script.main()

script是类Loader的实例对象,而Loader里也并没有main方法,它是怎么做到不报错的呢?这里就用到了__getattr__。

__getattr__ 是在属性查找过程中最后用来兜底的,如果在类里找不到,就会到父类中寻找,最后都找不到时会调用__getattr__来查找,因此我们可以重写这个方法来实现一些功能

class Demo():
    def __init__(self, name):
        self.name = name

    def __getattr__(self, item):
        if item == 'age':
            return 14
        else:
            def do_not_has():
                print('你查找的属性不存在')

            return do_not_has

demo = Demo('实例')
print(demo.age)        # 14
demo.show()            # 你查找的属性不存在

在源码中,__getattr__是这样定义的

def __getattr__(self, attr):
    return getattr(self.module, attr)

self.module是被加载的模块,因此script.main(), 实际上是调用的self.module 的main函数,在script.py文件中,则应当实现一个main函数。

3. 总结

hotreload虽然短小但是精悍,源码只有60多行,却包含了很多技术细节,值得研究学习,从实际使用的角度来看,这个模块只适用于检测那些需要一直运行的脚本,而且有修改的需求。

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案