如何使用装饰器将变量注入作用域?

72

[免责声明:可能有更符合Python习惯的做法,但是我想知道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()

我希望它打印出 "Message",但实际上它给出了:

NameError: global name 'var' is not defined

回溯甚至指向了变量 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

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

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

11个回答

70

你做不到。作用域名(闭包)在编译时就已经确定,你不能在运行时添加更多。

你能做的最好结果是通过函数的自有全局命名空间添加全局名称:

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

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

这是因为函数中未分配且未在周围范围内找到的任何名称都标记为全局名称。

演示:

>>> c = 'Message'
>>> @decorator_factory(c)
... def msg_printer():
...     print var
... 
>>> msg_printer()
Message
>>> 'var' in globals()
False

但是与其装饰它,我也可以直接在全局范围内定义var

请注意,更改全局变量不是线程安全的,并且在同一模块中对其他函数的短暂调用仍将看到此相同的全局变量。


所以如果我执行 def msg_printer(): print var 并尝试调用 msg_printer,我会得到相同的名称错误,但是如果我定义 var='Hi' 然后调用它,它就可以正常打印。在这个例子中,varmsg_printer 编译之后的运行时没有被定义吗? - beardc
由于在函数或父级作用域中未定义var,因此在编译时,var被标记为全局名称。但是,如果存在父级作用域,则在编译时,var将被标记为作用域名称,此时装饰器技巧也将不再起作用。 - Martijn Pieters
1
@ArtyomLisovskij 这就是为什么我的答案最后包括警告:注意修改全局变量不是线程安全的 - Martijn Pieters
1
@martineau:将来维护使用此装饰器的代码库的人不会感谢您在其运行后留下此装饰器的影响;这里的代码滥用了全局命名空间,以实现全局命名空间并非真正设计用途的功能。如果您要这样做,最好尽量减少滥用。想象一下,如果必须向模块添加一个新的全局变量,那么它将会“神秘地不断更改”。您会知道查看该模块中方法上使用的装饰器吗? - Martijn Pieters
@martineau:这只是对模块全局字典的引用:vars(sys.modules[f.__module__]) is f.__globals__(通常,一些项目会替换__module__引用,例如SQLalchemy,它使用元编程来帮助文档编写)。 - Martijn Pieters
显示剩余4条评论

10

这里有一种将多个变量注入函数作用域的方法,类似于@Martijn Pieters在他的回答中所做的。我发布它主要是因为它是一个更通用的解决方案,并且不需要像他的(以及许多其他)答案那样多次应用。

值得注意的是,在修饰函数和namespace字典之间形成了闭包,因此更改其内容 - 例如namespace['a'] = 42 - 影响对该函数的后续调用。

from functools import wraps

def inject_variables(context):
    """ Decorator factory. """

    def variable_injector(func):
        """ Decorator. """
        @wraps(func)
        def decorator(*args, **kwargs):
            func_globals = func.__globals__

            # Save copy of any global values that will be replaced.
            saved_values = {key: func_globals[key] for key in context
                                                        if key in func_globals}
            func_globals.update(context)
            try:
                result = func(*args, **kwargs)
            finally:
                func_globals.update(saved_values)  # Restore replaced globals.

            return result

        return decorator

    return variable_injector


if __name__ == '__main__':
    namespace = dict(a=5, b=3)

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

    test()

9

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

使用“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()

这在概念上与传递位置参数有何不同? - Mad Physicist
4
虽然没有太大的区别,但由于它是为位置参数编写的,您需要知道参数的位置。作为装饰器,您却不知道它的位置。 在我看来,kwargs 是最安全的方式,因为您可以控制参数的名称。 - M07
1
@M07 - 我正要发布这个完全相同的解决方案,但我是那些在发布之前阅读的人之一!唯一的缺点是装饰函数必须包括**kwargs,但这并不是很糟糕,并且装饰器和被装饰函数之间可能存在关键字名称冲突,但可以管理。 - user3481644

6
您无法这样做。Python采用词法作用域。这意味着当您查看源代码时,标识符的含义仅基于物理上包围它的范围确定。

4

更新 __globals__ 对我来说有效。

def f():
    print(a)


def with_context(**kw):
    def deco(fn):
        g = fn.__globals__
        g.update(kw)
        return fn

    return deco


with_context(a=3)(f)() # 3

3
这里是一个简单的示例,使用装饰器将一个变量添加到函数的作用域中。
>>> def add_name(name):
...     def inner(func):
...         # Same as defining name within wrapped
...         # function.
...         func.func_globals['name'] = name
...
...         # Simply returns wrapped function reference.
...         return func
... 
...     return inner
...
>>> @add_name("Bobby")
... def say_hello():
...     print "Hello %s!" % name
...
>>> print say_hello()
Hello Bobby!
>>>

6
请注意,您在此处操作的是一个共享字典。同一模块中的其他函数也会看到这个更改,而且修改字典不是线程安全的。 - Martijn Pieters
@MartijnPieters 即使修饰器修改的值不再被修改,而是在修饰器返回后仅被读取,这是否仍然是一个问题? - langlauf.io
1
@stackoverflowwww:每当调用该函数时,它都会修改模块全局变量。 - Martijn Pieters
@MartijnPieters,那么为什么人们不总是使用Alexander Otavka和M07上下建议的方法,即让装饰器使用参数调用被装饰函数,从而将值传递给被装饰函数呢? - langlauf.io
1
@stackoverflowwww:大多数人确实使用这样的技术。 - Martijn Pieters

2
我发现一篇有趣的帖子,提供了一种通过即时创建函数来实现不同解决方案的方法。基本上是这样的:
def wrapper(func):
    cust_globals = func.__globals__.copy()

    # Update cust_globals to your liking

    # Return a new function
    return types.FunctionType(
        func.__code__, cust_globals, func.__name__, func.__defaults__, func.__closure__
    )

请查看 https://hardenedapple.github.io/stories/computers/python_function_override/


2

Python采用词法作用域,所以恐怕没有干净的方法可以在没有一些可能有害的副作用的情况下完成您想要的操作。 我建议通过装饰器将var传递到函数中。

c = 'Message'

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

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

msg_printer()  # prints 'Message'

1
msg_decorator.__name__ = f.__name__msg_decorator.__doc__ = f.__doc__ 是用来做什么的?它们是必要的吗? - langlauf.io
3
在Python中,每个函数都有一个名称(除非使用lambda创建),而且许多函数都有文档字符串。这两者都对生成文档非常重要,所以我们将它们复制到包装函数中。我在我的答案中犯了一个错误,实际上应该将它们复制到“inner_dec”上。 - Alexander Otavka
3
请参考functools.wraps函数来完成这样的操作。 - killthrush
从其他答案中可以看出,有“干净”的方法来完成它... - martineau

0
假设在Python中函数是对象,那么你可以...
#!/usr/bin/python3


class DecorClass(object):
    def __init__(self, arg1, arg2):
        self.a1 = arg1
        self.a2 = arg2

    def __call__(self, function):
        def wrapped(*args):
            print('inside class decorator >>')
            print('class members: {0}, {1}'.format(self.a1, self.a2))
            print('wrapped function: {}'.format(args))
            function(*args, self.a1, self.a2)
        return wrapped


    @DecorClass(1, 2)
    def my_function(f1, f2, *args):
        print('inside decorated function >>')
        print('decorated function arguments: {0}, {1}'.format(f1, f2))
        print('decorator class args: {}'.format(args))


    if __name__ == '__main__':
        my_function(3, 4)

结果是:

inside class decorator >>
class members: 1, 2
wrapped function: (3, 4)
inside decorated function >>
decorated function arguments: 3, 4
decorator class args: (1, 2)

这里有更多的解释http://python-3-patterns-idioms-test.readthedocs.io/en/latest/PythonDecorators.html


0
def merge(d1, d2):
    d = d1.copy()
    d.update(d2)
    return d

# A decorator to inject variables
def valueDecorator(*_args, **_kargs):
    def wrapper(f):
        def wrapper2(*args, **kargs):
            return f(*args, **kargs)
        wrapper2.__name__ = f.__name__
        wrapper2.__doc__ = f.__doc__
        oldVars = getattr(f, 'Vars', [])
        oldNamedVars = getattr(f, 'NamedVars', {})
        wrapper2.Vars = oldVars + list(_args)
        wrapper2.NamedVars = merge(oldNamedVars, _kargs)
        return wrapper2
    return wrapper

@valueDecorator(12, 13, a=2)
@valueDecorator(10, 11, a=1)
def func():
    print(func.Vars)
    print(func.NamedVars)

与其修改全局范围,更合理的做法是修改被注释的函数本身。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接