AberSheeran
Aber Sheeran
I know nothing except the fact of my ignorance.

Python文件的热重载

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

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

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

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

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

那么两者组合起来就能达到重载的目的,以下仅为实现代码。实际运用的代码可参考index/watchdog

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()

通过监听文件系统的事件来import或者reload import对应的模块,看起来很完美。但实际上,这一方法可能没有想象中的那么好,它仍有一些副作用。

不可变性

在Web程序中,保持旧的请求仍然按照旧有的代码执行是比较重要的。而reload过程中的不可变性很好的提供了这一功能。

类与实例

如果一个类的实例已经被创建,那么重新加载定义类的模块不会影响实例的定义——它们继续使用旧的类定义。派生类(子类)也是如此。

局限性

通过importlib文档介绍,可以看到这一方法仍然有它的局限性。

内存回收

reload这一操作并不会影响GC的正常进行,对象仍然需要等待引用计数为零时才会被回收。

全局变量

reload一个模块之后,这个模块会被再次执行一遍。如果这个模块里初始化了一些全局变量,那么这些全局变量将会再次被初始化。

为了解决这个问题,可以通过如下方式定义模块。

例如模块中拥有一个全局变量cache

try:
    cache
except NameError:
    cache = {}

如果reload之后的模块里去除了原有模块中定义的一些变量,那么在reload后、并不会去除这些部分,也就是说reload只做更新或者增加,而不会删除。

导入问题

如果一个模块使用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过程并不对原对象作释放、替换等动作。故而导入模块来代替导入对象能解决这个问题,因为此时是通过模块的属性来调用对象,在本模块中没有任何的引用。

旧版本Python

在更早版本的Python中,imp是一个可行的替代品。

等我有空,可能会写一套兼容impimportlib的代码
to be contine......

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