使用装饰器更改函数代码并使用 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]
但前提是 args
和 kwargs
已经声明。我希望在执行 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 中的操作方法。
我正在尝试应用一个更改函数代码的装饰器,然后使用更改后的代码执行此函数。
下面是带有示例功能的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]
但前提是 args
和 kwargs
已经声明。我希望在执行 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 中的操作方法。