Python inspect.getsource在装饰器中使用时抛出错误

gwo2fgha  于 2023-03-11  发布在  Python
关注(0)|答案(1)|浏览(251)

我有以下功能

def foo():
    for _ in range(1):
        print("hello")

现在我想添加另一个print语句,在每次循环迭代后打印“Loop iterated”,为此我定义了一个新函数,将foo转换为ast树,插入相应的print节点,然后将ast树编译成一个可执行函数:

def modify(func):

    def wrapper():
        
        source_code = inspect.getsource(func)
        ast_tree = ast.parse(source_code)

        # insert new node into ast tree
        for node in ast.walk(ast_tree):
            if isinstance(node, ast.For):
                node.body += ast.parse("print('Loop iterated')").body

        # get the compiled function
        new_func = compile(ast_tree, '<ast>', 'exec')
        namespace = {}
        exec(new_func, globals(), namespace)
        new_func = namespace[func.__name__]
        
        return new_func()

    return wrapper

在使用以下项时,此功能可以正常工作:

foo = modify(foo)
foo()

但是,如果我决定使用modify作为装饰器:

@modify
def foo():
    for _ in range(1):
        print("hello")
foo()

出现以下错误:

Traceback (most recent call last):
  File "c:\Users\noinn\Documents\decorator_test\test.py", line 34, in <module>
    foo()
  File "c:\Users\noinn\Documents\decorator_test\test.py", line 25, in wrapper
    return new_func()
  File "c:\Users\noinn\Documents\decorator_test\test.py", line 11, in wrapper
    source_code = inspect.getsource(func)
  File "C:\Users\noinn\AppData\Local\Programs\Python\Python39\lib\inspect.py", line 1024, in getsource
    lines, lnum = getsourcelines(object)
  File "C:\Users\noinn\AppData\Local\Programs\Python\Python39\lib\inspect.py", line 1006, in getsourcelines
    lines, lnum = findsource(object)
  File "C:\Users\noinn\AppData\Local\Programs\Python\Python39\lib\inspect.py", line 835, in findsource
    raise OSError('could not get source code')
OSError: could not get source code

有人知道为什么会出现这个错误吗?请注意,如果我返回原始函数,并且错误只在调用new_func()时出现,则不会出现这种情况。
------------------------解决方案---------
只需使用以下命令将装饰器从装饰器本身的函数中移除:

ast_tree.body[0].decorator_list = []
dldeef67

dldeef671#

经过一些实验,我发现我最初的假设是不正确的:inspect.getsource足够聪明,即使在模块globals中还没有设置函数名,也可以检索源代码(令人惊讶)。
所发生的事情是,源代码与 decorator 调用沿着被检索,当函数被调用时,decorator再次运行-因此,它在inspect.getsource中获得了一些可重入性,在这些点上它失败了。
解决方案如下:我只是在检索到的源代码中剥离模块级的装饰器,然后将其提供给ast.parse
我还重新安排了装饰器,因为它将重新读取源代码,并在每次调用装饰函数时重新解析AST--按照本例的方式,根本不需要内部wrapper函数。如果您碰巧需要参数化装饰器,并且需要内部wrapper,所有函数重写部分,直到创建new_func,应该在 Package 之外,这样它们就只运行一次

import inspect
import ast
import functools  

def modify(func):
    func.__globals__[func.__name__] = func
    source_lines = inspect.getsource(func).splitlines()
    # strip decorators from the source itself
    source_code = "\n".join(line for line in source_lines if not line.startswith('@'))

    ast_tree = ast.parse(source_code)

    # insert new node into ast tree
    for node in ast.walk(ast_tree):
        if isinstance(node, ast.For):
            node.body += ast.parse("print('Loop iterated')").body

    # get the compiled function
    new_func = compile(ast_tree, '<ast>', 'exec')
    namespace = {}
    exec(new_func, func.__globals__, namespace)
    new_func = namespace[func.__name__]

    return functools.wraps(func)(new_func)

@modify
def foo():
    for _ in range(1):
        print("hello")

#foo = modify(foo)

foo()

初步答复

在此进行推理和更简单的解决方法:
它不能获得<module>.foo函数的源代码,原因很简单,即名称foo只会在函数定义(包括所有装饰器)执行之后定义和绑定。
在装饰器代码运行时,名称foo并不作为变量存在于模块中,尽管此时它被设置为func中的__name__属性,但inspect.getsource对此无能为力。
换句话说,如果使用的是inspect.getsource,装饰器语法是完全不受限制的,因为getsource还不能获取任何源代码。
好消息是解决方法很简单:我们必须放弃@装饰器语法,并“以旧的方式”应用装饰器(就像Python 2.3之前那样):一个像往常一样声明函数,并将其名称重新分配给手动调用装饰器的返回值--这与你的工作示例中所写的相同:

def foo(...):
    ...

foo = modify(foo)

相关问题