Python运行程序的热插拔

18 投票
3 回答
10670 浏览
提问于 2025-04-16 21:51

下面的代码让你可以在运行时修改 runtime.py 的内容。换句话说,你不需要暂停 runner.py 的运行。

#runner.py
import time
import imp

def main():
    while True:
        mod = imp.load_source("runtime", "./runtime.py")
        mod.function()
        time.sleep(1)

if __name__ == "__main__":
    main()

在运行时导入的模块是:

# runtime.py
def function():
    print("I am version one of runtime.py")

这个简单的机制让你可以“热替换”Python代码(类似于Erlang)。有没有更好的方法呢?

请注意,这只是一个学术性的问题,因为我现在并不需要做这样的事情。不过,我对了解Python的运行时有兴趣。

补充说明

我创建了一个解决方案:一个 Engine 对象提供了一个接口,用于访问模块中的函数(在这个例子中,模块叫做 engine.py)。这个 Engine 对象还会启动一个线程,监控源文件的变化,如果检测到变化,就会调用引擎的 notify() 方法,重新加载源文件。

在我的实现中,变化检测是通过每 frequency 秒轮询一次,检查文件的SHA1校验和来完成的,但也可以有其他实现方式。

在这个例子中,每次检测到变化都会记录到一个叫 hotswap.log 的文件中,里面会记录校验和。

检测变化的其他机制可以是一个服务器,或者在 Monitor 线程中使用 inotify

import imp
import time
import hashlib
import threading
import logging

logger = logging.getLogger("")

class MonitorThread(threading.Thread):
    def __init__(self, engine, frequency=1):
        super(MonitorThread, self).__init__()
        self.engine = engine
        self.frequency = frequency
        # daemonize the thread so that it ends with the master program
        self.daemon = True 

    def run(self):
        while True:
            with open(self.engine.source, "rb") as fp:
                fingerprint = hashlib.sha1(fp.read()).hexdigest()
            if not fingerprint == self.engine.fingerprint:
                self.engine.notify(fingerprint)
            time.sleep(self.frequency)

class Engine(object):
    def __init__(self, source):
        # store the path to the engine source
        self.source = source        
        # load the module for the first time and create a fingerprint
        # for the file
        self.mod = imp.load_source("source", self.source)
        with open(self.source, "rb") as fp:
            self.fingerprint = hashlib.sha1(fp.read()).hexdigest()
        # turn on monitoring thread
        monitor = MonitorThread(self)
        monitor.start()

    def notify(self, fingerprint):
        logger.info("received notification of fingerprint change ({0})".\
                        format(fingerprint))
        self.fingerprint = fingerprint
        self.mod = imp.load_source("source", self.source)

    def __getattr__(self, attr):
        return getattr(self.mod, attr)

def main():
    logging.basicConfig(level=logging.INFO, 
                        filename="hotswap.log")
    engine = Engine("engine.py")
    # this silly loop is a sample of how the program can be running in
    # one thread and the monitoring is performed in another.
    while True:
        engine.f1()
        engine.f2()
        time.sleep(1)

if __name__ == "__main__":
    main()

engine.py 文件:

# this is "engine.py"
def f1():
    print("call to f1")

def f2():
    print("call to f2")

日志示例:

INFO:root:received notification of fingerprint change (be1c56097992e2a414e94c98cd6a88d162c96956)
INFO:root:received notification of fingerprint change (dcb434869aa94897529d365803bf2b48be665897)
INFO:root:received notification of fingerprint change (36a0a4b20ee9ca6901842a30aab5eb52796649bd)
INFO:root:received notification of fingerprint change (2e96b05bbb8dbe8716c4dd37b74e9f58c6a925f2)
INFO:root:received notification of fingerprint change (baac96c2d37f169536c8c20fe5935c197425ed40)
INFO:root:received notification of fingerprint change (be1c56097992e2a414e94c98cd6a88d162c96956)
INFO:root:received notification of fingerprint change (dcb434869aa94897529d365803bf2b48be665897)

再说一次,这只是一个 学术性 的讨论,因为我现在并不需要热替换Python代码。不过,我喜欢能够理解一点运行时的知识,明白什么是可能的,什么是不可能的。请注意,加载机制可以添加锁,以防使用资源时出现问题,并且可以进行异常处理,以防模块加载失败。

有什么意见吗?

3 个回答

0

如果你想要在代码中热更新,也就是在使用导入功能时能够实时更新代码,你需要覆盖模块的一个全局变量。比如说,你可以这样做:

import mylib

在代码中加载模块时,你需要把新的模块赋值给mylib这个变量。还有一个问题是,如果你的程序使用了线程,你需要测试一下在多线程环境下是否安全。而在使用多进程时,这个新代码只会在一个进程中生效。如果你想让所有进程都能使用新代码,就必须在每个进程中都加载新的代码,因此需要检查在多进程环境下是否安全。

另外,检查一下是否有新代码也是很有意思的,这样就可以避免重复加载相同的代码。需要注意的是,在Python中,你只能加载一个新的模块并替换模块的变量名。如果你真的需要一个很好的热更新功能,可以看看Erlang语言和它的OTP框架,效果非常不错。

1
globe = __import__('copy').copy(globals())
while True:
    with open('runtime.py', 'r') as mod:
        exec mod in globe
    __import__('time').sleep(1)

这个代码会反复读取并运行 runtime.py 文件,使用几乎干净的 globals(),而且没有 locals()。这样做不会污染全局变量的范围,但 runtime 中的所有内容都会在 globe 中可用。

7

你可以监控 runtime.py 文件,等它发生变化。一旦变化了,就调用

reload(runtime)

每次我在调试一个 Python 模块时,我都会在交互式的 Python 命令提示符下使用这种方法(不过我会手动调用 reload(),不去监控任何东西)。

编辑: 要检测文件的变化,可以看看 这个问题。监控可能是最可靠的选择,但我只会在文件的修改时间更新时才重新加载,而不是每次监控都重新加载。你还应该考虑在重新加载时捕捉异常,特别是语法错误。而且你可能会遇到线程安全的问题,也可能不会。

撰写回答