简体   繁体   English

Python:猴子修补函数的源代码

[英]Python: monkey patch a function's source code

Can I add a prefix and suffix to the source code of functions? 我可以在函数的源代码中添加前缀和后缀吗?

I know about decorators and do not want to use them (the minimal example below doesn't make clear why, but I have my reasons). 我知道装饰器并且不想使用它们(下面的最小例子没有说明原因,但我有我的理由)。

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

Here is what I have so far: 这是我到目前为止:

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

Unfortunately, nothing happens if I use this and then call g() as above; 不幸的是,如果我使用它然后如上所述调用g() ,则没有任何反应; neither world nor Hello, world! 无论是world还是worldHello, world! are printed. 印刷。

Here is a rough version of what can be done: 这是一个可以做的粗略版本:

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!

The call of compile compiles an entire module (represented by tree ). compile调用compile整个模块(由tree表示)。 This module is then executed in an empty namespace from which the desired function is finally extracted. 然后在空命名空间中执行该模块,从该空命名空间最终提取所需的函数。 (Warning: the namespace will need to be filled with some globals from where f comes from if f uses those.) (警告:如果f使用f ,命名空间将需要填充f来自哪些全局变量。)


After some more work, here is a real example of what can be done with this. 经过一些更多的工作,这里有一个真正的例子,说明可以做些什么。 It uses some extended version of the principle above: 它使用了上面原理的一些扩展版本:

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

The clou is that I get the exact same output if I reshuffle the parts of my_algorithm , for example like this: 如果我重新调整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

This works by (1) grabbing the source of my_algorithm and turning it into an ast (2) patching each function defined within my_algorithm (eg SumFirstArguments) to return locals (3) deciding based on the inputs and the outputs (as defined by the type hints) in which order the parts of my_algorithm should be executed. 这可以通过(1)抓取my_algorithm的源并将其转换为ast(2)修补my_algorithm定义的每个函数(例如SumFirstArguments)来返回基于输入和输出决定的本地(3)(由类型定义)提示)以何种顺序执行my_algorithm的各个部分。 Furthermore, a possibility that I do not have implemented yet is to execute independent parts in parallel (such as SumFirstArguments and SinOfThird ). 此外,我还没有实现的可能性是并行执行独立的部分(例如SumFirstArgumentsSinOfThird )。 Let me know if you want the sourcecode of graphexecute , I haven't included it here because it contains a lot of stuff that is not relevant to this question. 如果你想要graphexecute的源代码,请graphexecute我,我没有把它包含在这里,因为它包含了很多与这个问题无关的东西。

For your problem, you don't need to recompile your functions. 对于您的问题,您不需要重新编译您的功能。 Just define a list of functions, you inspect for arguments and return variable name: 只需定义一个函数列表,检查参数并返回变量名称:

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))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM