使用装饰器更改函数代码并使用 eval 执行它?

Changing function code using a decorator and execute it with eval?

我正在尝试应用一个更改函数代码的装饰器,然后使用更改后的代码执行此函数。

下面是带有示例功能的temp模块。如果将 some_decorator 应用于此函数,我只是希望函数 return [*args, *kwargs.items(), 123] 而不是 [*args, *kwargs.items()]

编辑:请注意,这只是一个玩具示例,我不打算将新值附加到列表中,而是重写一大块函数。

from inspect import getsource

def some_decorator(method):
    def wrapper(*args, **kwargs):
        source_code = getsource(method)
        code_starts_at = source_code.find('):') + 2
        head = source_code[:code_starts_at]
        body = source_code[code_starts_at:]

        lines = body.split('\n')
        return_line = [i for i in lines if 'return' in i][0]
        old_expr = return_line.replace('    return ', '')
        new_expr = old_expr.replace(']', ', 123]')

        new_expr = head + '\n' + '    return ' + new_expr

        return eval(new_expr)
    return wrapper

@some_decorator
def example_func(*args, *kwargs):
    return [*args, *kwargs]

再解释一下:我正在重写原来的函数

def example_func(*args, **kwargs):
    return [*args, *kwargs.items()]

def example_func(*args, **kwargs):
    return [*args, *kwargs.items(), 123]

希望eval能够编译运行这个修改后的函数

当我尝试 运行 时,它 return 是一个语法错误。

from temp import example_func
example_func(5)

我知道 eval 可以解决这个问题:

[*args, *kwargs.items(), 123]

但前提是 argskwargs 已经声明。我希望在执行 example_func.

时从 example_func(args, kwargs) 读取它们

我想只要将修改后的函数代码写入文件即可

def example_func(*args, **kwargs):
    return [*args, *kwargs.items(), 123]

并使 some_decorator 使用修改后的代码而不是原始代码来执行函数,这样就可以正常工作。但是,理想情况下我会跳过创建任何中间文件。

有可能实现吗?

虽然从技术上讲,您可以使用 Python 中的函数和装饰器做任何事情,但您 不应该

在这种特定情况下,向 return 列表的函数添加额外值非常简单:

def some_decorator(method):
    def wrapper(*args, **kwargs):
        result = method(*args, **kwargs)
        return result + [123]
    return wrapper

这不需要重写任何功能代码。如果您所做的只是更改函数的输入或输出,只需更改输入或输出,并保留函数本身。

装饰器在这里主要只是语法糖,一种改变

的方法
def function_name(*args, **kwargs):
    # ...

function_name = create_a_wrapper_for(function_name)

进入

@create_a_wrapper_for
def function_name(*args, **kwargs):
    # ...

还要注意eval() function can't alter your fuction, because eval() is strictly limited to expressions. The def syntax to create a function is a statement。从根本上说,语句可以包含表达式和其他语句(例如 if <test_expression>: <body of statements>),但表达式不能包含语句。这就是为什么您会收到 SyntaxError 异常; [*args, *kwargs.items()] 是一个有效的表达式,return [*args, *kwargs.items()] 是一个语句(包含一个表达式):


>>> args, kwargs = (), {}
>>> eval("[*args, *kwargs.items()]")
[]
>>> eval("return [*args, *kwargs.items()]")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    return [*args, *kwargs.items()]
         ^
SyntaxError: invalid syntax

要将文本作为任意 Python 代码执行,您必须改用 exec() function,并注意使用与原始函数相同的命名空间,以便原始函数中使用的任何全局变量仍然可以访问。

例如,如果函数调用另一个函数来获取额外的值:

def example(*args, **kwargs):
    return [extra_value(), *args, *kwargs.items()]

def extra_value():
    return 42

那么你就不能单独执行example()函数;它是模块全局命名空间的一部分,并在您调用该函数时在该命名空间中查找 extra_value。函数具有对创建它们的模块的全局命名空间的引用,可通过 function.__globals__ 属性访问。当你使用 exec() 执行一个 def 创建函数的语句时,那么新的函数对象连接到你传入的全局命名空间。注意 def 创建一个函数对象并赋值给它到函数名称,因此您必须再次从同一名称空间检索该对象:

>>> namespace = {}
>>> exec("def foo(): return 42", namespace)
>>> namespace["foo"]
<function foo at 0x7f8194fb1598>
>>> namespace["foo"]()
42
>>> namespace["foo"].__globals__ is namespace
True

接下来,重建 Python 代码的文本操作效率非常低且容易出错。例如,如果函数改用此代码,您的 str.replace() 代码将失败:

def example(*args, **kwargs):
    if args or kwargs:
        return [
            "[called with arguments:]",
            *args,
            *kwargs.items()
        ]

因为现在return进一步缩进,列表中的字符串值中有[..]括号,列表的结束]括号完全在单独的一行.

将源代码 Python 编译成 抽象语法树 (通过 ast module)会更好,然后再进行处理树。定义明确的对象的有向图比文本更容易操作(在使用多少空格等方面更加灵活)。上面的代码和您的示例都会生成一个带有 Return() 节点的树,该节点包含一个表达式,其顶级是 List() 节点。您可以遍历该树并找到所有 Return() 个节点并更改它们的 List() 个节点,在列表内容的末尾添加一个额外的节点。

A Python AST 可以编译为代码对象(使用 compile())然后 运行 到 exec()(它不仅接受文本,还接受代码对象作为嗯)。

有关重写 Python 代码的项目的真实示例,请查看 how pytest rewrites the assert statement to add extra context。他们使用模块导入挂钩来执行此操作,但只要函数的源代码可用,您也可以使用装饰器来执行此操作。

下面是使用 ast 模块更改 return 语句中的列表的示例,添加任意常量:

import ast, inspect, functools

class ReturnListInsertion(ast.NodeTransformer):
    def __init__(self, value_to_insert):
        self.value = value_to_insert

    def visit_FunctionDef(self, node):
        # remove the `some_decorator` decorator from the AST
        # we don’t need to keep applying it.
        if node.decorator_list:
            node.decorator_list = [
                n for n in node.decorator_list
                if not (isinstance(n, ast.Name) and n.id == 'some_decorator')
            ]
        return self.generic_visit(node)

    def visit_Return(self, node):
        if isinstance(node.value, ast.List):
            # Python 3.8 and up have ast.Constant instead, which is more
            # flexible.
            node.value.elts.append(ast.Num(self.value))
        return self.generic_visit(node)

def some_decorator(method):
    source_code = inspect.getsource(method)
    tree = ast.parse(source_code)

    updated = ReturnListInsertion(123).visit(tree)
    # fix all line number references, make it match the original
    updated = ast.increment_lineno(
        ast.fix_missing_locations(updated),
        method.__code__.co_firstlineno
    )

    ast.copy_location(updated.body[0], tree)

    # compile again, as a module, then execute the compiled bytecode and
    # extract the new function object. Use the original namespace
    # so that any global references in the function still work.
    code = compile(tree, inspect.getfile(method), 'exec')
    namespace = method.__globals__
    exec(code, namespace)
    new_function = namespace[method.__name__]

    # update new function with old function attributes, name, module, documentation
    # and attributes.
    return functools.update_wrapper(new_function, method)

请注意,此 不需要包装函数。您不需要每次尝试调用它时都重新处理一个函数,装饰器可以只调用一次,然后 return 直接生成函数对象。

这里有一个演示模块可以用来试用:

@some_decorator
def example(*args, **kwargs):
    return [extra_value(), *args, *kwargs.items()]

def extra_value():
    return 42

if __name__ == '__main__':
    print(example("Monty", "Python's", name="Flying circus!"))

当运行.

时以上输出[42, 'Monty', "Python's", ('name', 'Flying circus!'), 123]

不过,只用第一种方法就简单多了。

如果您确实想继续使用 exec() 和 AST 操作,我建议您阅读 Green Tree Snakes 中的操作方法。