阅读本文大概需要 3.6 分钟。
有时候,我感到疲倦,因为,我每修改一处代码,想要看到改动是否生效的时候,我要先 Ctrl C
或 Kill
进程,然后重新运行,才能看到结果,改的次数多了,不仅浪费时间,降低效率,还浪费体力。有没有办法做到修改了项目使用的源码文件后,让程序自动重新运行?
肯定有办法,三方库 watchdog 可以监控文件的新增,删除,和修改,可以在这些事件发生后执行相应的动作,但它不够完美:
可以对某一路径进行监听,但不能解析项目 import 了哪些文件,import 的文件不在同一路径下,需要手工配置多个路径就很麻烦,不具有通用性。
不能判断文件是否真正的修改,有时候只是保存下,文件内容并没有变化,此时不应该触发重启。
如果在同一路径,修改了项目未引用的文件,也会触发重启。
直到我用了 Django,Django 的 autoreload 机制,完美的解决了上面 3 个问题,改动代码保存后可以立即看到程序的及时反馈,大大提升了 Debug 的效率,堪称神器。
这么好的神器,能否移植到其他项目上?
能否移植,取决于 autoreload 是否与 Django 松耦合,我们先来看一下它的工作原理。
用过 Django 的朋友都知道,当你执行 python manage.py runserver
后,只要修改了项目用到的文件,Django 会自动重新启动服务,这种及时反馈机制,大大的方便了开发者,可以快速确认自己的修改是否正确,为测试省了不少时间。
从 Django(Django==3.0.4) 的源码 django/core/management/commands/runserver.py
走起,执行 runserver 命令后就执行了下面这个 run 函数。
def run(self, **options):
"""Run the server, using the autoreloader if needed."""
use_reloader = options['use_reloader']
if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options)
self.inner_run
是真正干活的,先不管它。执行命令时如果不加 --noreload
,就会运行 autoreload.run_with_reloader
,我们继续追踪到 django/utils/autoreload.py
def run_with_reloader(main_func, *args, **kwargs):
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get(DJANGO_AUTORELOAD_ENV) == 'true':
reloader = get_reloader()
logger.info('Watching for file changes with %s', reloader.__class__.__name__)
start_django(reloader, main_func, *args, **kwargs)
else:
exit_code = restart_with_reloader()
sys.exit(exit_code)
except KeyboardInterrupt:
pass
run_with_reloader 函数第一行 signal.SIGTERM 来捕捉用户输入 kill 指令,让程序退出并返回 0。接下来就是判断环境变量是 DJANGO_AUTORELOAD_ENV 是否为 true,如果是,执行 start_django,否则执行 restart_with_reloader。默认设置情况下,第一次运行时,环境变量是没有设置的,因此会运行 restart_with_reloader
def restart_with_reloader():
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: 'true'}
args = get_child_arguments()
while True:
p = subprocess.run(args, env=new_environ, close_fds=False)
if p.returncode != 3:
return p.returncode
restart_with_reloader 先通过 get_child_arguments
获取命令及参数,再进入循环,通过 subprocess.run
来运行 Django 服务,Django 运行的过程中,函数是阻塞在此处的,Django 进程运行结束返回的结果不是 3,程序直接就退出了。
p = subprocess.run(args, env=new_environ, close_fds=False)
大家猜测下 Django 进程什么时候返回 3 呢?相信你已经猜到了,就是文件有修改时,trigger_reload 函数让 Django 进程返回了 3,通过循环,实现重新启动的效果。
def trigger_reload(filename):
logger.info('%s changed, reloading.', filename)
sys.exit(3)
调用这个函数的类为 StatReloader 和 WatchmanReloader,具体的细节见 py37env/lib/python3.7/site-packages/django/utils/autoreload.py
理解了工作原理后,就可以为我所用了。
好在 django.utils.autoreload
和 django 其他模块是松耦合的,不需要修改代码即可可以直接移植到其他项目使用。做法很简单,只需要将 Django 库中 utils 目录下的 autoreload.py 文件复制到自己项目的路径下,再导入使用即可。
两行代码就可以实现,我这里做了个 demo:
demo 目录树如下:
(py37env) ➜ test tree
.
├── autoreload.py
├── test.py
└── test2.py
0 directories, 3 files
test.py 文件内容如下:
# filename: test.py
import autoreload
import test2
def main():
print("---------------------")
print("test.main1")
print("test.main2")
print("test.main3")
test2.main()
if __name__ == '__main__':
autoreload.run_with_reloader(main)
test2.py 文件内容如下:
def main():
print("test2.main11")
print("test2.main22")
print("test2.main33")
运行 python test.py 后,程序打印了预期的结果,但没有退出,说明 autoreload 内部是以守护进程方式运行主函数 main。修改 test.py test2.py 的任何地方,程序都会重新运行,非常便于调试。如果只保存,未修改任何内容,则程序不会重新运行,非常智能。
运行结果如下:
---------------------
test.main1
test.main2
test.main3
test2.main11
test2.main22
test2.main33
---------------------
test.main1
test.main2
test.main3
test.main4
test2.main11
test2.main22
test2.main33
test2.main44
视频展示:https://b23.tv/MAqqLK(点击阅读原文观看)
源代码我放在了公众号后台,如果不想动手找 Django 源码 autoreload ,可以直接回复关键词「autoreload」获取下载连接。