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

sha*_*mar 1 python eval decorator

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

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

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

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]
Run Code Online (Sandbox Code Playgroud)

多一点解释:我正在重写原始函数

def example_func(*args, **kwargs):
    return [*args, *kwargs.items()]
Run Code Online (Sandbox Code Playgroud)

def example_func(*args, **kwargs):
    return [*args, *kwargs.items(), 123]
Run Code Online (Sandbox Code Playgroud)

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

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

from temp import example_func
example_func(5)
Run Code Online (Sandbox Code Playgroud)

我知道eval能够解决这个问题:

[*args, *kwargs.items(), 123]
Run Code Online (Sandbox Code Playgroud)

但前提是argskwargs已经声明。我希望它们example_func(args, kwargs)在我执行时被读取example_func

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

def example_func(*args, **kwargs):
    return [*args, *kwargs.items(), 123]
Run Code Online (Sandbox Code Playgroud)

some_decorator使用修改后的代码而不是原始代码来执行该功能,会很好地工作。但是,理想情况下我会跳过创建任何中间文件。

有可能实现吗?

Mar*_*ers 5

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

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

def some_decorator(method):
    def wrapper(*args, **kwargs):
        result = method(*args, **kwargs)
        return result + [123]
    return wrapper
Run Code Online (Sandbox Code Playgroud)

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

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

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

function_name = create_a_wrapper_for(function_name)
Run Code Online (Sandbox Code Playgroud)

进入

@create_a_wrapper_for
def function_name(*args, **kwargs):
    # ...
Run Code Online (Sandbox Code Playgroud)

另请注意,该eval()函数 不能改变您的功能,因为eval()它严格限于表达式def创建函数的语法是语句。从根本上说,语句可以包含表达式和其他语句(例如if <test_expression>: <body of statements>),但表达式不能包含语句。这就是为什么您会遇到SyntaxError异常的原因;while[*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
Run Code Online (Sandbox Code Playgroud)

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

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

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

def extra_value():
    return 42
Run Code Online (Sandbox Code Playgroud)

那么你就不能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
Run Code Online (Sandbox Code Playgroud)

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

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

因为 nowreturn进一步缩进,[..]列表中的字符串值中有括号,列表的右]括号完全位于单独的行上。

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

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

对于重写 Python 代码的项目的真实示例,请查看pytest 如何重写assert语句以添加额外的上下文。他们使用模块导入挂钩来执行此操作,但只要源代码可用于函数,您也可以使用装饰器来执行此操作。

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

def some_decorator(method):
    def wrapper(*args, **kwargs):
        result = method(*args, **kwargs)
        return result + [123]
    return wrapper
Run Code Online (Sandbox Code Playgroud)

请注意,这不需要包装函数。每次尝试调用函数时都不需要重新工作,装饰器可以只执行一次并直接返回结果函数对象。

这是一个演示模块,可用于尝试:

@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!"))
Run Code Online (Sandbox Code Playgroud)

[42, 'Monty', "Python's", ('name', 'Flying circus!'), 123]运行时的上述输出。

但是,仅使用第一种方法要容易得多。

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