hotreload可以监测python脚本的源码是否发生变动,如果确认源码发生改变,hotreload模块可以重新加载脚本并执行脚本。
该项目git地址: https://github.com/say4n/hotreload ,阅读本文,你将有如下收获:
假设你有一个名为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使用起来很简单,它只能监测一个脚本的源码是否发生改变。
源码只有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)
类 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)
类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就简单了,不需要传入字符串,只需要将需要重新加载的模块传入其中就可以了。
这里有必要重点解释一下__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函数。
hotreload虽然短小但是精悍,源码只有60多行,却包含了很多技术细节,值得研究学习,从实际使用的角度来看,这个模块只适用于检测那些需要一直运行的脚本,而且有修改的需求。
QQ交流群: 211426309