如何使用装饰器将变量注入范围?

bea*_*rdc 50 python closures scope decorator python-decorators

[免责声明:可能有更多pythonic方式做我想做的事情,但我想知道python的范围如何在这里工作]

我正在尝试找到一种方法来制作一个装饰器,它可以将名称注入到另一个函数的范围内(这样名称不会泄漏到装饰器的范围之外).例如,如果我有一个函数说要打印一个名为var尚未定义的变量,我想在调用它的装饰器中定义它.这是一个打破的例子:

c = 'Message'

def decorator_factory(value):
    def msg_decorator(f):
        def inner_dec(*args, **kwargs):
            var = value
            res = f(*args, **kwargs)
            return res
        return inner_dec
    return msg_decorator

@decorator_factory(c)
def msg_printer():
    print var

msg_printer()
Run Code Online (Sandbox Code Playgroud)

我想要打印" Message",但它给出了:

NameError: global name 'var' is not defined
Run Code Online (Sandbox Code Playgroud)

回溯甚至指向wher var定义:

<ipython-input-25-34b84bee70dc> in inner_dec(*args, **kwargs)
      8         def inner_dec(*args, **kwargs):
      9             var = value
---> 10             res = f(*args, **kwargs)
     11             return res
     12         return inner_dec
Run Code Online (Sandbox Code Playgroud)

所以我不明白为什么找不到var.

有没有办法做这样的事情?

Mar*_*ers 54

你不能.范围名称(闭包)是在编译时确定的,您无法在运行时添加更多.

您可以希望实现的最好的方法是使用函数自己的全局命名空间添加全局名称:

def decorator_factory(value):
    def msg_decorator(f):
        def inner_dec(*args, **kwargs):
            g = f.__globals__  # use f.func_globals for py < 2.6
            sentinel = object()

            oldvalue = g.get('var', sentinel)
            g['var'] = value

            try:
                res = f(*args, **kwargs)
            finally:
                if oldvalue is sentinel:
                    del g['var']
                else:
                    g['var'] = oldvalue

            return res
        return inner_dec
    return msg_decorator
Run Code Online (Sandbox Code Playgroud)

f.__globals__是包装函数的全局命名空间,因此即使装饰器位于不同的模块中也是如此.如果var已经定义为全局,则将其替换为新值,并在调用该函数后恢复全局变量.

这是有效的,因为函数中未分配给的任何名称在周围范围内找不到,而是标记为全局.

演示:

>>> c = 'Message'
>>> @decorator_factory(c)
... def msg_printer():
...     print var
... 
>>> msg_printer()
Message
>>> 'var' in globals()
False
Run Code Online (Sandbox Code Playgroud)

但是,我可以直接var在全球范围内定义,而不是装饰.

请注意,更改全局变量不是线程安全的,并且对同一模块中的其他函数的任何瞬态调用仍将看到相同的全局变量.


M07*_*M07 7

有一种干净的方法可以在不使用全局变量的情况下完成您想要的操作。如果你想要无状态和线程安全,你真的没有选择。

使用“kwargs”变量:

c = 'Message'

def decorator_factory(value):
    def msg_decorator(f):
    def inner_dec(*args, **kwargs):
        kwargs["var"] = value
        res = f(*args, **kwargs)
        return res
    return inner_dec
return msg_decorator

@decorator_factory(c)
def msg_printer(*args, **kwargs):
    print kwargs["var"]

msg_printer()
Run Code Online (Sandbox Code Playgroud)

  • 它并没有那么不同,但是因为它是为位置参数而编写的,您应该知道您的参数的位置。因为它是一个装饰器,你不知道它。**kwargs** 是最安全的方法,因为您可以控制参数的名称。 (3认同)

mar*_*eau 6

这是一种将多个变量注入函数范围的方法,其方式类似于@Martijn Pieters在其答案所做的。我主要张贴,因为它是一个更广泛的解决方案,就不会需要使用多次这样做-正如他(和许多其他的)答案是必需的。

from functools import wraps

def inject_variables(context):
    """ Decorator factory. """
    def variable_injector(func):
        @wraps(func)
        def decorator(*args, **kwargs):
            try:
                func_globals = func.__globals__  # Python 2.6+
            except AttributeError:
                func_globals = func.func_globals  # Earlier versions.

            saved_values = func_globals.copy()  # Shallow copy of dict.
            func_globals.update(context)

            try:
                result = func(*args, **kwargs)
            finally:
                func_globals = saved_values  # Undo changes.

            return result

        return decorator

    return variable_injector

if __name__ == '__main__':
    namespace = {'a': 5, 'b': 3}

    @inject_variables(namespace)
    def test():
        print('a:', a)
        print('b:', b)

    test()
Run Code Online (Sandbox Code Playgroud)


new*_*cct 5

你不能.Python有词法范围.这意味着标识符的含义仅基于查看源代码时物理上围绕它的范围来确定.