Python:Monkey修补函数的源代码

时间:2018-12-16 12:35:56

标签: python abstract-syntax-tree monkeypatching

我可以在函数的源代码中添加前缀和后缀吗?

我了解装饰器并且不想使用它们(下面的最小示例并不能弄清楚为什么,但是我有我的理由)。

def f():
    print('world')
g = patched(f,prefix='print("Hello, ");',suffix='print("!");')
g() # Hello, world!

这是我到目前为止所拥有的:

import inspect
import ast
import copy
def patched(f,prefix,suffix):
    source = inspect.getsource(f)
    tree = ast.parse(source)
    new_body = [
        ast.parse(prefix).body[0],
        *tree.body[0].body,
        ast.parse(suffix).body[0]
    ]
    tree.body[0].body = new_body
    g = copy.deepcopy(f)
    g.__code__ = compile(tree,g.__code__.co_filename,'exec')
    return g

不幸的是,如果我使用它,然后如上所述调用g(),则什么也不会发生; worldHello, world!均未打印。

2 个答案:

答案 0 :(得分:3)

这是可以做的事情的粗略版本:

import inspect
import ast
import copy
def patched(f,prefix,suffix):
    source = inspect.getsource(f)
    tree = ast.parse(source)
    new_body = [
        ast.parse(prefix).body[0],
        *tree.body[0].body,
        ast.parse(suffix).body[0]
    ]
    tree.body[0].body = new_body
    code = compile(tree,filename=f.__code__.co_filename,mode='exec')
    namespace = {}
    exec(code,namespace)
    g = namespace[f.__name__]
    return g

def temp():
    pass
def f():
    print('world',end='')
g = patched(f,prefix='print("Hello, ",end="")',suffix='print("!",end="")')
g() # Hello, world!

compile的调用将编译整个模块(由tree表示)。然后,在一个空的命名空间中执行此模块,最终从该命名空间中提取所需的功能。 (警告:如果f使用这些全局名称,则命名空间将需要填充f来自的某些全局名称。)


经过更多工作,这是使用此功能可以完成的真实示例。它使用了上述原理的一些扩展版本:

import numpy as np
from playground import graphexecute
@graphexecute(verbose=True)
def my_algorithm(x,y,z):
    def SumFirstArguments(x,y)->sumxy:
        sumxy = x+y
    def SinOfThird(z)->sinz:
        sinz = np.sin(z)
    def FinalProduct(sumxy,sinz)->prod:
        prod = sumxy*sinz
    def Return(prod):
        return prod
print(my_algorithm(x=1,y=2,z=3)) 
#OUTPUT:
#>>Executing part SumFirstArguments
#>>Executing part SinOfThird
#>>Executing part FinalProduct
#>>Executing part Return
#>>0.4233600241796016

关键是,如果我重新组合my_algorithm的各个部分,我将得到完全相同的输出,例如:

@graphexecute(verbose=True)
def my_algorithm2(x,y,z):
    def FinalProduct(sumxy,sinz)->prod:
        prod = sumxy*sinz
    def SumFirstArguments(x,y)->sumxy:
        sumxy = x+y
    def SinOfThird(z)->sinz:
        sinz = np.sin(z)
    def Return(prod):
        return prod
print(my_algorithm2(x=1,y=2,z=3)) 
#OUTPUT:
#>>Executing part SumFirstArguments
#>>Executing part SinOfThird
#>>Executing part FinalProduct
#>>Executing part Return
#>>0.4233600241796016

这是通过(1)抓取my_algorithm的源并将其转换为ast(2)修补my_algorithm中定义的每个函数(例如SumFirstArguments)以返回本地变量(3)来决定的输入和输出(由类型提示定义)应按my_algorithm的顺序执行。此外,我尚未实现的一种可能性是并行执行独立的部分(例如SumFirstArgumentsSinOfThird)。让我知道是否需要graphexecute的源代码,因为它包含许多与此问题无关的内容,所以我没有在此处包括它。

答案 1 :(得分:1)

对于您的问题,您无需重新编译函数。只需定义一个函数列表,检查参数并返回变量名:

def FinalProduct(sumxy, sinz) -> "prod":
    return sumxy * sinz

def SumFirstArguments(x, y) -> "sumxy":
    return x + y

def SinOfThird(z) -> "sinz":
    return np.sin(z)

def execute(funcs, **args):
    result = None
    while funcs:
        func = funcs.pop(0)
        try:
            kw = {a: args[a]
                for a in func.__code__.co_varnames[:func.__code__.co_argcount]
            }
        except KeyError:
            # not all arguments found
            funcs.append(func)
        else:
            print(func,kw)
            result = func(**kw)
            args[func.__annotations__['return']] = result
    return result

print(execute([FinalProduct, SumFirstArguments, SinOfThird], x=1,y=2,z=3))