在创造Index.py的时候,我思考了一个问题——如何使得Python程序像一个传统PHP服务一样,在保证现有的请求不变的情况下,新请求使用更新后的代码进行处理。
一开始我是为了模拟PHP一样的热重载,所以处理方式就如同PHP一样,在被请求时重新装载一遍文件。但这样的问题就是,非处理请求的文件不会被重新加载。
前者能监听指定路径下的文件系统事件,后者可以通过模块名来导入/重载指定的模块。
那么两者组合起来就能达到重载的目的:
import os
import threading
import importlib
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
class MonitorFileEventHandler(FileSystemEventHandler):
def dispatch(self, event):
if not event.src_path.endswith(".py"):
return
event.filepath = os.path.relpath(event.src_path, Config().path).replace("\\", "/")[:-3]
if event.filepath.endswith("/__init__"):
event.filepath = event.filepath[:-len("/__init__")]
return super().dispatch(event)
def on_modified(self, event):
module_path = ".".join(event.filepath.split("/"))
def reload():
module = importlib.import_module(module_path)
importlib.reload(module)
threading.Thread(target=reload, daemon=True).start()
def on_created(self, event):
module_path = ".".join(event.filepath.split("/"))
threading.Thread(target=importlib.import_module, args=(module_path, ), daemon=True).start()
class MonitorFile:
def __init__(self):
self.observer = Observer()
self.observer.schedule(MonitorFileEventHandler(), Config().path, recursive=True)
self.observer.start()
def __del__(self):
"""drop observer"""
self.observer.stop()
self.observer.join()
不可变性
在Web程序中,保持旧的请求仍然按照旧有的代码执行是比较重要的。而reload
过程中的不可变性很好的提供了这一功能。
如果一个类的实例已经被创建,那么重新加载定义类的模块不会影响实例的定义——它们继续使用旧的类定义。派生类(子类)也是如此。
局限性
通过importlib文档介绍,可以看到这一方法仍然有它的局限性。
内存回收
reload
这一操作并不会影响GC的正常进行,对象仍然需要等待引用计数为零时才会被回收。
全局变量
reload一个模块之后,这个模块会被再次执行一遍。如果这个模块里初始化了一些全局变量,那么这些全局变量将会再次被初始化。
为了解决这个问题,可以通过如下方式定义模块。
例如模块中拥有一个全局变量cache
try:
cache
except NameError:
cache = {}
如果reload
之后的模块里去除了原有模块中定义的一些变量,那么在reload
后、并不会去除这些部分,也就是说reload
只做更新或者增加,而不会删除。原因很简单——Python 中的对象删除由 GC 控制而不是程序员。
导入问题
如果一个模块使用from index.config import logger
这种形式导入其他模块中的对象,给index.config
调用reload()
不会变更logger
对象——解决这个问题的一种方式是重新执行from
语句,另一种方式是使用 import
和限定名称(module.name)来代替。
前一种方法就是把from
语句写在需要用到它的函数或者类里;而后一种则是指使用from index import config
代替from index.config import logger
——导入模块而不是导入模块内的对象。
这个问题其实不难理解,当我们使用from ... import ...
导入一个对象时,在本模块中创建了一个对其他模块中对象的引用,而reload
过程并不对原对象作释放、替换等动作。故而导入模块来代替导入对象能解决这个问题,因为此时是通过模块的属性来调用对象,在本模块中没有任何的引用。
实际运用
可以通过建立模块之间的互相依赖关系映射来解决导入问题——当一个模块被重载之后,只要将整个项目里通过from ... import
方式依赖此模块的模块也进行重载,这样就可以解决导入问题。
故而实际使用里唯一的问题只有:含有初始化全局变量代码的模块如何重载?
面对这个问题的解决方案是:拒绝编写任何含有初始化全局变量的代码。但如果真的需要使用全局变量,那么应该在项目代码之外、也就是第三方依赖里提供一个可存储全局变量的对象,这样只针对项目代码的热重载就不会影响到全局变量。
在 index.py/autoreload.py 中可以看到实际可用的代码。
旧版本Python
在更早版本的Python中,imp是一个可行的替代品。
等我有空,可能会写一套兼容imp
与importlib
的代码 (可能得等我去接手 Python2 的项目并且有这个需求的时候才会写了,从 CSIG 出来之后,对老代码就有了恐惧感,如果生活过得去,我这辈子也不会去维护 Python2 的代码,毕竟现在我连维护 3.6 的代码都有点不是很爽)
to be contine......