Python 函数运行时更新

Python 动态修改(运行时更新)

特性

  1. 实现函数运行时动态修改(开发的时候,非线上)
  2. 支持协程(tornado等)
  3. 兼容 python2, python3

安装

pip install realtimefunc

使用

from realtimefunc import realtimefunc

@coroutine
@realtimefunc
def test():
    # function body

引言

后端服务的启动一般需要做相当多的准备工作, 导致启动的速度比较慢, 而在开发一项任务中很难做到一次性通过, 所以可能需要反复的重启服务, 这样相当的恼人。

另外, 接触一个 python 项目时, 当代码表示的逻辑难以理解, 或对代码表示的逻辑和实际效果有疑问的时候, 通常需要通过打印 log 的方式来理解, 或者通过 pdb 调试理解。
前者比较难以避免少打部分 log 而需要重启服务,后者功能强大但使用起来比较复杂,而且不能使当次修改,在下次调用及时生效(需要重启服务)。

备注:

很多web框架有自启动, 是通过检测项目文件的 mtime , 然后替换掉当前的服务进程,比如 tornado 就是用一个定时器定时检测项目文件, 实现 autostart。但这种重启的是整个服务,所以费时并不会减少。

这样很自然的就会想到, 如果可以随时改动开发的代码, 而不需要重启整个服务,就舒服多了。

实现目标:
实现一个装饰器, 被装饰的函数任意修改,无需重启服务,再次调用时及时生效。

实现思路:
实现一个装饰器, 被装饰函数实际不会被调用, 只提供入口(被装饰函数)。 装饰器通过入口提供的信息, 找到入口定义源代码,获取最新的代码, 定义成一个当前作用域的内存函数。实际运行的则是这个内存函数。最新代码 realtimefunc

"""A decorator is used to update a function at runtime."""

from __future__ import print_function
import sys
import os
import re
import ast
import linecache
import functools
import traceback
from inspect import getfile, isclass, findsource, getblock

__all__ = ["realtimefunc"]

Decorator = "@realtimefunc"

PY3 = sys.version_info >= (3,)


# The cache

# record, used to record functions decorated by realtimefunc,
# is a dict {filename:{func1, func2}
# refresh, used to to mark functions which source file stat has changed,
# is also a dict {filename:{func1, func2}}

# Note: the filename may be repeated but it doesn't matter.

record = {}
refresh = {}


def _exec(code, filepath, firstlineno, glob, loc=None):
    astNode = ast.parse(code)
    astNode = ast.increment_lineno(astNode, firstlineno)
    code = compile(astNode, filepath, 'exec')
    exec(code, glob, loc)


def _findclass(func):
    cls = sys.modules.get(func.__module__)
    if cls is None:
        return None
    for name in func.__qualname__.split('.')[:-1]:
        cls = getattr(cls, name)
    if not isclass(cls):
        return None
    return cls


def get_qualname(func):
    '''return qualname by look through the call stack.'''
    qualname = []
    stacks = traceback.extract_stack(f=None)
    begin_flag = False
    for stack in stacks[::-1]:
        if stack[3].strip() == Decorator:
            qualname.append(func.__name__)
            begin_flag = True
        if stack[2] == '<module>':
            break
        if begin_flag:
            qualname.append(stack[2])
    return '.'.join(qualname[::-1])


def get_func_real_firstlineno(func):
    start_lineno = 0
    lines = linecache.getlines(func.__code__.co_filename)
    cls = _findclass(func)
    if cls:
        lines, lnum = findsource(cls)
        lines = getblock(lines[lnum:])
        start_lineno = lnum

    #  referenced from inspect _findclass
    pat = re.compile(r'^(\s*)def\s*' + func.__name__ + r'\b')
    candidates = []
    for i in range(len(lines)):
        match = pat.match(lines[i])
        if match:
            # if it's at toplevel, it's already the best one
            if lines[i][0] == 'd':
                return (i + start_lineno)
            # else add whitespace to candidate list
            candidates.append((match.group(1), i))
    if candidates:
        # this will sort by whitespace, and by line number,
        # less whitespace first
        candidates.sort()
        return start_lineno + candidates[0][1]
    else:
        raise OSError('could not find function definition')


def get_source_code(func, func_runtime_name, firstlineno):
    lines = linecache.getlines(func.__code__.co_filename)
    code_lines = getblock(lines[firstlineno:])
    # repalce function name
    code_lines[0] = code_lines[0].replace(func.__name__, func_runtime_name, 1)
    i_indent = code_lines[0].index("def")
    # code indentation
    code_lines = [line[i_indent:] for line in code_lines]
    code = ''.join(code_lines)
    return code


def check_file_stat(filename):
    entry = linecache.cache.get(filename, None)
    change = False
    if not entry:
        change = True
    else:
        size, mtime, _, fullname = entry
        try:
            stat = os.stat(fullname)
        except OSError:
            change = True
            del linecache.cache[filename]
        if size != stat.st_size or mtime != stat.st_mtime:
            change = True
            del linecache.cache[filename]

    if change:
        global refresh
        for f in record[filename]:
            refresh.setdefault(filename, set()).add(f)


def realtimefunc(func):
    # python2 need set __qualname__ by hand
    if not PY3:
        func.__qualname__ = get_qualname(func)
    func_real_name = func.__qualname__.replace('.', '_') + '_realfunc'
    filename = getfile(func)
    filepath = os.path.abspath(filename)
    global record, refresh
    record.setdefault(filename, set()).add(func)
    refresh.setdefault(filename, set()).add(func)


    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        glob = func.__globals__
        check_file_stat(filename)
        if func in refresh[filename]:
            firstlineno = get_func_real_firstlineno(func)
            code_str = get_source_code(func, func_real_name, firstlineno)
            _exec(code_str, filepath, firstlineno, glob)
            refresh[filename].remove(func)
        func_realtime = glob[func_real_name]
        return func_realtime(*args, **kwargs)

    return wrapper

效果
实现开发运行时修改函数, 可以很方便的查看和修改被装饰函数相关的数据,以及构造简单测试数据和进行简单的分支测试。
TODO

  • 支持 log 打印, log 打印时, 显示的文件为 <string>, 行号也是相对的。 已实现
  • 内存函数可以优化为只有在改动的时候重新定义,也就是可以做缓存。 - 已实现

posted on 2018-08-22 14:35  nowg  阅读(967)  评论(0编辑  收藏  举报

导航