Python运行程序的热插拔

18
以下代码允许您在运行时修改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")

这种基本机制允许您像Erlang一样实现Python代码的“热交换”。有没有更好的替代方案?

请注意,这只是一个学术性问题,因为我不需要做任何类似的事情。然而,我对了解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代码的需求。然而,我喜欢能够稍微理解运行时,并意识到什么是可能的,什么是不可能的。请注意,加载机制可以添加锁,以防它使用资源,还可以添加异常处理,以防模块加载失败。

有任何评论吗?


2
不要将答案编辑到你的问题中,而是将其作为答案添加。 - agf
1
可能是如何卸载(重新加载)Python模块?的重复问题。 - user6876082
3个回答

7
你可以轮询runtime.py文件,等待它发生变化。一旦发生变化,只需调用即可。
reload(runtime)

任何时候我在调试Python模块时,都会在交互式Python命令提示符中使用这种方法(除非手动调用reload(),我不轮询任何内容)。
编辑: 要检测文件中的更改,请查看这个this SO question。轮询可能是最可靠的选项,但我只会在更新修改时间时重新加载文件,而不是在每次轮询时重新加载。您还应考虑在重新加载时捕获异常,特别是语法错误。您可能会遇到线程安全问题或者也可能不会。

1
事实上,函数reload和imp.load_source是等效的。我猜我更感兴趣的是代理重新加载机制的“轮询”或“监听器”机制。谢谢! - Escualo
请注意,对于Python 3.4+,您需要使用“from importlib import reload”,而对于早期版本,则需要使用“from imp import reload”。 - Robert

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 中可用。


1
这很有趣,但我不会失去命名空间吗? - Escualo
2
是的,你需要。我假设 runner.py 只是一个助手,可以重复从 runtime.py 中重新加载和执行代码,因此你不在意。你需要制作 globals() 的副本,然后进行编辑以显示它。 - agf

0
如果您想要热交换代码,并在使用函数等导入时找到它,您需要覆盖模块的全局变量。例如,如果您使用:
import mylib

在代码中加载模块时,您需要将新模块分配给mylib。另一个问题是,在使用线程的程序中尝试了解是否与线程安全,并且当使用多进程时,仅在一个进程中找到此类情况,为了在所有进程中更改代码,需要加载新代码,必须尝试是否在多进程中安全。

而且,首先检查是否有新代码,以免加载相同的代码。并且请注意,仅在Python中可以加载新模块并替换模块的变量名称,但如果您确实需要良好的热更改代码,请查看Erlang语言和OTP,它非常出色。


1
使用Erlang/OTP建议是当需要热代码替换、容错软件、并发或分布式执行时的最佳解决方案。WhatsApp在Erlang上运行服务器,Facebook的聊天也同样如此,Tuenti和Call of Duty服务器等也都是如此。 - user6876082

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接