• 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*)defs*' + func.__name__ + r'')
        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>, 行号也是相对的。 已实现
    • 内存函数可以优化为只有在改动的时候重新定义,也就是可以做缓存。 - 已实现
  • 相关阅读:
    Java的一些命名规范
    Java利用泛型实现堆栈
    Java 将二进制打印成十六进制
    String对象的一些基本方法
    Java异常使用指南
    JAVAEE期末项目------文章发布系统
    java14周
    java第11周
    java第八周作业
    java第七周----json
  • 原文地址:https://www.cnblogs.com/nowg/p/9517478.html
Copyright © 2020-2023  润新知