流行的python web框架都支持自动重载技术,虽然该技术不能应用于生产环境,但在开发测试过程中,极大的提升了开发人员的工作效率,启动服务以后,如果对源码进行了修改,框架会自动重新加载所有代码,这样,就免去了反复停止启动服务的麻烦。
本文将带你研究这种自动重载技术,探究其内部实现的原理
python 内置函数reload可以将一个模块重新导入,通过学习理解这个函数,你可以知晓框架重载的一般原理和过程,新建两个python脚本
├── __init__.py
├── conf.py
└── main.py
main.py的内容为
import time
import os
import conf
from threading import Thread
from imp import reload
def run():
"""
主流程
:return:
"""
while True:
print(conf.ip)
time.sleep(5)
def watch():
"""
监控文件变化
:return:
"""
last_modify_time = get_file_modify_time('./conf.py')
while True:
modify_time = get_file_modify_time('./conf.py')
if modify_time != last_modify_time:
last_modify_time = modify_time
reload(conf)
time.sleep(3)
def get_file_modify_time(filename):
return time.ctime(os.stat(filename).st_mtime)
t = Thread(target=watch)
t.start() # 启动文件监控
run() # 启动主流程
conf.py的内容为
ip = '192.168.0.1'
在main.py脚本中,run函数启动后,每5秒钟会打印一次conf模块下的ip值,同时,watch函数会在另外一个线程中监控conf.py的脚本是否发生变化,一旦发现conf.py脚本的最后修改时间由变化,则使用reload函数重新加载conf模块。
启动main.py脚本后,等待它输出几次ip的值以后,修改conf.py里ip变量的值,你会发现,main.py里输出的值也随之发生变化,这说明,reload函数重新导入了conf模块,因此conf.ip的值发生了改变。
上面的例子虽然简单,但它揭示了python web框架自动重载的一般原理,即监控文件变化,重新载入模块。
为了演示自动重载技术,我写了一个简单的服务
脚本目录为 /Users/zhangdongsheng/experiment/test/test.py
from flask import Flask
app = Flask(__name__)
@app.route('/', methods=['GET'])
def index():
return 'ok'
app.run(debug=True)
启动服务后,你可以修改index函数的返回值,可以看到flask会自动重新加载脚本
通过跟踪run方法,我们得到以下函数调用链
run --> run_simple --> run_with_reloader -->
先来看run_with_reloader
def run_with_reloader(main_func, extra_files=None, interval=1,
reloader_type='auto'):
"""Run the given function in an independent python interpreter."""
import signal
reloader = reloader_loops[reloader_type](extra_files, interval)
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
t = threading.Thread(target=main_func, args=())
t.setDaemon(True)
t.start()
reloader.run()
else:
sys.exit(reloader.restart_with_reloader())
except KeyboardInterrupt:
pass
在run_simple函数中,定义了一个inner函数,这个函数就是真正的启动服务的函数,inner函数作为参数传给run_with_reloader,参数名为main_func
在run_with_reloader 函数中,先要判断环境变量run_with_reloader 是否为true,如果不等于字符串true,则执行代码
sys.exit(reloader.restart_with_reloader())
这是理解flask自动重载的关键所在,sys.exit意味着进程退出,但在退出前,先执行了restart_with_reloader方法,reloader对象的类型是StatReloaderLoop,来看一下restart_with_reloader方法都做了哪些事情
def restart_with_reloader(self):
"""Spawn a new Python interpreter with the same arguments as this one,
but running the reloader thread.
"""
while 1:
_log('info', ' * Restarting with %s' % self.name)
args = _get_args_for_reloading()
new_environ = os.environ.copy()
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
# a weird bug on windows. sometimes unicode strings end up in the
# environment and subprocess.call does not like this, encode them
# to latin1 and continue.
if os.name == 'nt' and PY2:
for key, value in iteritems(new_environ):
if isinstance(value, text_type):
new_environ[key] = value.encode('iso-8859-1')
exit_code = subprocess.call(args, env=new_environ,
close_fds=False)
if exit_code != 3:
return exit_code
_get_args_for_reloading() 函数返回的结果是
['/usr/local/bin/python3.6', '/Users/zhangdongsheng/experiment/test/test.py']
前面是python的解释器位置,后面是我实验代码的脚本位置, 通过subprocess.call方法,再一次的启动了服务脚本,这就意味着我的test.py脚本再次被执行,而之前执行时所启动的进程即将在执行sys.exit时结束。
另一个关键点是在这个方法中,将环境变量WERKZEUG_RUN_MAIN设置为true,这样,当代码再次执行到run_with_reloader时,if条件语句就成立了
由于if条件成立,这次,会启动一个线程来执行main_func函数,同时,执行reloader.run()
class StatReloaderLoop(ReloaderLoop):
name = 'stat'
def run(self):
mtimes = {}
while 1:
for filename in chain(_iter_module_files(),
self.extra_files):
try:
mtime = os.stat(filename).st_mtime
except OSError:
continue
old_time = mtimes.get(filename)
if old_time is None:
mtimes[filename] = mtime
continue
elif mtime > old_time:
self.trigger_reload(filename)
self._sleep(self.interval)
代码意图很明显,就是要检查服务所依赖的每一个模块是否有变化,一旦有变化,就执行trigger_reload方法,trigger_reload方法会记录输出重载的脚本名称,同时执行sys.exit(3),进程要退出,退出?进程退出了还怎么重载啊?
回到restart_with_reloader方法中,注意看最后一段
exit_code = subprocess.call(args, env=new_environ,
close_fds=False)
if exit_code != 3:
return exit_code
当前进程是由subprocess.call创建的,如果通过sys.exit(3)退出,那么就会返回状态码3赋值给exit_code, 可是只有在exit_code不等于3的情况下,才会执行return,当前进程退出是由于文件被修改了,exit_code=3,那么restart_with_reloader方法就不会被退出,而是继续执行while循环,再一次执行subprocess.call方法启动服务。
通过前面的分析,你应该大值理解flask框架的自动重载原理了,简单总结一下
QQ交流群: 211426309