AberSheeran
Aber Sheeran

Python文件的热重载

起笔自
所属文集: 程序杂记
共计 3059 个字符
落笔于

在创造Index.py的时候,我思考了一个问题——如何使得Python程序像一个传统PHP服务一样,在保证现有的请求不变的情况下,新请求使用更新后的代码进行处理。

一开始我是为了模拟PHP一样的热重载,所以处理方式就如同PHP一样,在被请求时重新装载一遍文件。但这样的问题就是,非处理请求的文件不会被重新加载。

然后我想到了watchdogimportlib这两个库。

前者能监听指定路径下的文件系统事件,后者可以通过模块名来导入/重载指定的模块。

那么两者组合起来就能达到重载的目的:

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是一个可行的替代品。

等我有空,可能会写一套兼容impimportlib的代码 (可能得等我去接手 Python2 的项目并且有这个需求的时候才会写了,从 CSIG 出来之后,对老代码就有了恐惧感,如果生活过得去,我这辈子也不会去维护 Python2 的代码,毕竟现在我连维护 3.6 的代码都有点不是很爽)

to be contine......

如果你觉得本文值得,不妨赏杯茶
文件自动同步七牛云
Python的元类