Python 将变量添加到闭包的装饰器
我想写一个将自定义局部变量注入函数的装饰器 接口可能是这样的Python 将变量添加到闭包的装饰器,python,closures,decorator,Python,Closures,Decorator,我想写一个将自定义局部变量注入函数的装饰器 接口可能是这样的 def enclose(name, value): ... def decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator 期望: @enclose('param1', 1) def f():
def enclose(name, value):
...
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
期望:
@enclose('param1', 1)
def f():
param1 += 1
print param1
f() will compile and run without error
输出:
2
在python中可以这样做吗?为什么?您可以使用globals来完成,但我不推荐这种方法
def enclose(name, value):
globals()[name] = value
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
@enclose('param1', 1)
def f():
global param1
param1 += 1
print(param1)
f()
我想我会尝试一下,看看有多难。结果很难 第一件事是如何实现这一点?额外的
参数
是注入的局部变量、函数的附加参数或非局部变量。注入的局部变量每次都是一个新对象,但如何创建更复杂的对象。。。另一个参数将记录对象的变化,但在函数调用之间会忘记对名称的赋值。此外,这需要解析源代码以找到放置参数的位置,或者直接操作代码对象。最后,将变量声明为非局部变量将记录对象的变化和名称的赋值。实际上,非局部函数是全局的,但只能通过修饰函数到达。同样,使用非局部将需要解析源代码并找到放置非局部声明的位置或直接操纵代码对象
最后,我决定使用一个非局部变量并解析函数源。最初我打算操作代码对象,但它似乎太复杂了
以下是装饰器的代码:
import re
import types
import inspect
class DummyInject:
def __call__(self, **kwargs):
return lambda func: func
def __getattr__(self, name):
return self
class Inject:
function_end = re.compile(r"\)\s*:\s*\n")
indent = re.compile("\s+")
decorator = re.compile("@([a-zA-Z0-9_]+)[.a-zA-Z0-9_]*")
exec_source = """
def create_new_func({closure_names}):
{func_source}
{indent}return {func_name}"""
nonlocal_declaration = "{indent}nonlocal {closure_names};"
def __init__(self, **closure_vars):
self.closure_vars = closure_vars
def __call__(self, func):
lines, line_number = inspect.getsourcelines(func)
self.inject_nonlocal_declaration(lines)
new_func = self.create_new_function(lines, func)
return new_func
def inject_nonlocal_declaration(self, lines):
"""hides nonlocal declaration in first line of function."""
function_body_start = self.get_function_body_start(lines)
nonlocals = self.nonlocal_declaration.format(
indent=self.indent.match(lines[function_body_start]).group(),
closure_names=", ".join(self.closure_vars)
)
lines[function_body_start] = nonlocals + lines[function_body_start]
return lines
def get_function_body_start(self, lines):
line_iter = enumerate(lines)
found_function_header = False
for i, line in line_iter:
if self.function_end.search(line):
found_function_header = True
break
assert found_function_header
for i, line in line_iter:
if not line.strip().startswith("#"):
break
return i
def create_new_function(self, lines, func):
# prepares source -- eg. making sure indenting is correct
declaration_indent, body_indent = self.get_indent(lines)
if not declaration_indent:
lines = [body_indent + line for line in lines]
exec_code = self.exec_source.format(
closure_names=", ".join(self.closure_vars),
func_source="".join(lines),
indent=declaration_indent if declaration_indent else body_indent,
func_name=func.__name__
)
# create new func -- mainly only want code object contained by new func
lvars = {"closure_vars": self.closure_vars}
gvars = self.get_decorators(exec_code, func.__globals__)
exec(exec_code, gvars, lvars)
new_func = eval("create_new_func(**closure_vars)", gvars, lvars)
# add back bits that enable function to work well
# includes original global references and
new_func = self.readd_old_references(new_func, func)
return new_func
def readd_old_references(self, new_func, old_func):
"""Adds back globals, function name and source reference."""
func = types.FunctionType(
code=self.add_src_ref(new_func.__code__, old_func.__code__),
globals=old_func.__globals__,
name=old_func.__name__,
argdefs=old_func.__defaults__,
closure=new_func.__closure__
)
func.__doc__ = old_func.__doc__
return func
def add_src_ref(self, new_code, old_code):
return types.CodeType(
new_code.co_argcount,
new_code.co_kwonlyargcount,
new_code.co_nlocals,
new_code.co_stacksize,
new_code.co_flags,
new_code.co_code,
new_code.co_consts,
new_code.co_names,
new_code.co_varnames,
old_code.co_filename, # reuse filename
new_code.co_name,
old_code.co_firstlineno, # reuse line number
new_code.co_lnotab,
new_code.co_freevars,
new_code.co_cellvars
)
def get_decorators(self, source, global_vars):
"""Creates a namespace for exec function creation in. Must remove
any reference to Inject decorator to prevent infinite recursion."""
namespace = {}
for match in self.decorator.finditer(source):
decorator = eval(match.group()[1:], global_vars)
basename = match.group(1)
if decorator is Inject:
namespace[basename] = DummyInject()
else:
namespace[basename] = global_vars[basename]
return namespace
def get_indent(self, lines):
"""Takes a set of lines used to create a function and returns the
outer indentation that the function is declared in and the inner
indentation of the body of the function."""
body_indent = None
function_body_start = self.get_function_body_start(lines)
for line in lines[function_body_start:]:
match = self.indent.match(line)
if match:
body_indent = match.group()
break
assert body_indent
match = self.indent.match(lines[0])
if not match:
declaration_indent = ""
else:
declaration_indent = match.group()
return declaration_indent, body_indent
if __name__ == "__main__":
a = 1
@Inject(b=10)
def f(c, d=1000):
"f uses injected variables"
return a + b + c + d
@Inject(var=None)
def g():
"""Purposefully generate exception to show stacktraces are still
meaningful."""
create_name_error # line number 164
print(f(100)) # prints 1111
assert f(100) == 1111
assert f.__doc__ == "f uses injected variables" # show doc is retained
try:
g()
except NameError:
raise
else:
assert False
# stack trace shows NameError on line 164
其输出如下:
1111
Traceback (most recent call last):
File "inject.py", line 171, in <module>
g()
File "inject.py", line 164, in g
create_name_error # line number 164
NameError: name 'create_name_error' is not defined
1111
回溯(最近一次呼叫最后一次):
文件“inject.py”,第171行,在
g()
文件“inject.py”,第164行,g中
创建_name_error#行号164
名称错误:未定义名称“创建名称错误”
整件事丑陋得可怕,但它起作用了。还值得注意的是,如果方法使用了
Inject
,那么任何注入的值都会在类的所有实例之间共享。可能是这样,但为什么要这样做呢?如果函数需要参数(必须如此,因为它是按名称使用的),那么为什么不将其指定为实际参数呢?如果没有,为什么不使用正常的*args/**kwargs
语法呢?这是可能的,但不是你想做的事情。这将是各种奇怪的bug的来源,试图找出变量名来自何处或为什么它不存在。如果你这样做的话,另一个关键问题是:调用<代码> f-()的输出第二次是2还是3?另外,既然您将其称为param,您可以执行类似于f(param1=10)
的操作吗?这有什么意义?通过在函数中添加param1=1
作为语句,可以将装饰器的详细信息下移两行。这将获得同样的效果,而不需要发明奇怪且不可能实现的装饰器。解析源代码和字节码黑客之间有一步:解析AST。Python在stdlib中为此提供了帮助,尤其是。而且,在3.x中,可以相对轻松地构建一个导入钩子,在您导入的任何模块中自动为您应用transformer。但是,如果你真的想玩这样的东西,考虑一下,它有大量的酷帮助的特点。看起来棒极了。我知道正确的语法树解析器是更好的方法。但我这样做主要是为了加深自己对Python的理解。例如,答案中的代码花了我很长时间,但我刚刚在30分钟内完成了一个直接代码操作版本。不是因为这更容易,而是我对现在发生在引擎盖下的事情有了更多的了解。你确定PyMacro吗?看起来像是一个密钥重新绑定项目。哦,对不起,我是说,不是PyMacro。如果您想进一步了解Python,MacroPy是世界上最酷的东西之一。(特别是因为,如果你对文档中的某些内容感到困惑,李浩一经常会拿出一个新的500行脚本向你解释……)唯一需要注意的是,如果你想自己学习构建导入挂钩,并且不需要向后兼容,MacroPy的方式比3.4+的方式更加复杂和笨拙。但是仅仅把它作为一个框架,这不是问题。顺便说一句,如果你不知道MacroPy,你可能也不知道它(从1.5到2.3天都是死项目),这使得操作代码对象更容易。也可以看到,我写它比bytecodeasm
或BytecodeAssembler
更容易阅读和理解(但功能和健壮性相差甚远,所以不要把它当作示例代码使用)。