62

[免责声明:可能有更多的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()

我希望它打印“ 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

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

4

11 回答 11

64

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

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

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在全局范围内定义。

请注意,更改全局变量不是线程安全的,对同一模块中其他函数的任何临时调用也仍会看到相同的全局变量。

于 2013-07-25T15:28:26.943 回答
9

这是一种将多个变量注入函数范围的方法,其方式有点类似于@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()
于 2018-05-25T20:59:36.293 回答
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()
于 2016-11-30T17:44:21.687 回答
6

你不能。Python 具有词法作用域。这意味着标识符的含义完全取决于您查看源代码时物理上围绕它的范围。

于 2013-07-25T21:35:47.400 回答
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
于 2019-11-06T02:05:30.097 回答
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'
于 2015-08-12T05:02:15.373 回答
1

我发现一篇有趣的帖子通过动态创建函数提供了不同的解决方案。基本上:

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/

于 2020-03-27T11:41:01.567 回答
0

这是使用装饰器将变量添加到函数范围的简单演示。

>>> 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!
>>>
于 2015-06-18T15:59:56.407 回答
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)

与其修改全局作用域,不如改变注解函数本身更合理。

于 2019-03-09T17:26:38.257 回答
0

我发现使用全局变量的解决方案存在问题。

当您有多个并发请求时,全局变量的上下文可能会被覆盖。我认为这是不可能的,但它是 - 一段时间后,如果请求不快,我会发现上下文(全局)的变化。更好的解决方案是使用 kwargs 传递变量:

def is_login(old_fuction):
    def new_function(request, *args, **kwargs):
        secret_token = request.COOKIES.get('secret_token')
        if secret_token:
            items = SomeModel.objects.get(cookie = secret_token)
            if len(items) > 0:
                item = items[0]
                kwargs['current_user'] = item
                return old_fuction(request, *args, **kwargs)
            else:
                return HttpResponse('error')
        return HttpResponse(status=404)
    return new_function

@is_login  
def some_func(request, current_user):
    return HttpResponse(current_user.name)

您必须为每个修饰函数添加额外的参数。

于 2019-11-20T21:57:06.467 回答
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

于 2018-07-31T11:33:35.587 回答