Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/298.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python 将变量添加到闭包的装饰器_Python_Closures_Decorator - Fatal编程技术网

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
更容易阅读和理解(但功能和健壮性相差甚远,所以不要把它当作示例代码使用)。