Горячая замена запущенной программы Python

Следующий код позволяет вам изменять содержимое 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где контрольная сумма зарегистрирована.

Другими механизмами обнаружения изменений могут быть сервер или использование inotify в Monitor нить.

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 ответа

Вы можете опросить файл runtime.py, ожидая его изменения. Как только это изменится, просто позвоните

reload(runtime)

Каждый раз, когда я отлаживаю модуль python, я использую этот подход в интерактивной командной строке python (за исключением того, что я вручную вызываю reload(), я ничего не опрашиваю).

РЕДАКТИРОВАТЬ: Чтобы обнаружить изменения в файле, проверьте этот вопрос SO. Опрос может быть наиболее надежным вариантом, но я бы перезагрузил файл, только если измененное время обновляется, а не перезагружал его при каждом опросе. Вы должны также рассмотреть возможность перехвата исключений при перезагрузке, особенно синтаксических ошибок. И вы можете или не можете столкнуться с проблемами безопасности потоков.

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()и не будет загрязнять глобальную область, но все пространство имен среды выполнения будет доступно в globe

Если вам нужен код горячей замены, который был найден при импорте из функций и т. Д., Вам нужно перезаписать глобальную переменную модуля, если, например, вы используете:

import mylib

Вам нужно при загрузке модуля в коде присвоить mylib новый модуль. Другой вопрос - попробуйте это в программе, использующей потоки, чтобы узнать, защищен ли поток с потоками, и, если используется многопроцессорная обработка, это можно найти только в одном процессе, для изменения кода во всех процессах необходимо загрузить новый код, это необходимо, если это безопасно в многопроцессорных системах.

И, интересно сначала проверить, есть ли новый код или нет для того, чтобы не загружать тот же код. И подумайте, только в Python вы можете загрузить новый модуль и заменить имя переменной модуля, но если вам действительно нужен хороший горячий код изменения, посмотрите язык Erlang и OTP, это очень хорошо.

Другие вопросы по тегам