【问题标题】:How to inject variable into scope with a decorator?如何使用装饰器将变量注入范围?
【发布时间】:2013-07-25 14:12:31
【问题描述】:

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

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

【问题讨论】:

    标签: python scope closures decorator python-decorators


    【解决方案1】:

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

    您希望达到的最佳效果是添加全局名称,使用函数的自己的全局命名空间:

    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' 并调用它,它会很好地打印出来。在这个例子中,var 是不是在运行时没有被定义,在 msg_printer 被编译之后?
    • 因为var 没有在函数或父作用域中定义,var 在编译时被标记为全局名称。但如果有父作用域,那么在编译时,var 将被标记为作用域名称,此时装饰器技巧也不再起作用。
    • 当您有多个并发请求时,全局变量的上下文可能会被覆盖。我认为这是不可能的,但它是 - 我将此解决方案用于自定义用户身份验证(我的失败),如果请求没有快速处理,一段时间后会看到上下文更改。注意,现在我正在寻找新的解决方案。
    • @ArtyomLisovskij 这就是为什么我的回答最后会包含警告:请注意,更改全局变量不是线程安全的
    • @martineau:使用这种装饰器的代码库的未来维护者不会感谢你在运行后留下这个装饰器的效果;这里的代码滥用全局命名空间来实现全局命名空间并非真正设计的目的。如果你打算这样做,最好尽量减少滥用。想象一下,必须向一个模块添加一个新的全局变量,然后它会神秘地不断变化。您知道查看该模块中方法上使用的装饰器吗?
    【解决方案2】:

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

    【讨论】:

      【解决方案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!
      >>>
      

      【讨论】:

      • 请注意,您在此处操作的是共享字典。 同一模块中的其他函数也会看到此更改,并且更改字典不是线程安全的。
      • @MartijnPieters 即使​​装饰器修改的值不再被修改并且只在装饰器返回后读取,这是否有问题?
      • @stackoverflowwww:每当调用函数时,它都会修改模块全局变量。
      • @MartijnPieters 那么为什么人们不总是使用 Alexander Otavka 和 M07 上面和下面建议的方法,即让装饰器使用参数调用装饰函数,从而将值传递给装饰函数?
      • @stackoverflowwww:大多数人确实使用这种技术。
      【解决方案4】:

      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'
      

      【讨论】:

      • msg_decorator.__name__ = f.__name__msg_decorator.__doc__ = f.__doc__ 是干什么用的?有必要吗?
      • @stackoverflowwww python 中的每个函数都有一个名称(除非它是用 lambda 生成的),并且许多函数都有文档字符串。两者对于生成文档都很重要,因此我们将它们复制到包装函数中。我的回答有误,实际上应该将它们复制到inner_dec
      • 请参阅 functools.wraps 以了解此类事情
      • 从其他答案中可以明显看出,有“干净”的方法......
      【解决方案5】:

      有一种干净的方法可以在不使用全局变量的情况下做你想做的事。如果你想成为无状态和线程安全的,你真的别无选择。

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

      【讨论】:

      • 这在概念上与传递位置参数有何不同?
      • 它并没有太大的不同,但是因为它是为位置参数编写的,所以你应该知道你的参数的位置。因为它是一个装饰器,你不知道它。 kwargs 从观点来看是最安全的方式,因为您可以控制参数的名称。
      • 这对我来说无疑是一个足够好的解释来删除我的反对票。不过,不确定我是否可以在已接受的答案的上下文中对此表示赞同。
      【解决方案6】:

      这是一种将多个变量注入函数作用域的方法,其方式有点类似于@Martijn Pieters 在his answer 中所做的。我发布它主要是因为它是一个更通用的解决方案,并且不需要需要多次应用才能做到这一点 - 正如他(和许多其他)答案所要求的那样。

      需要注意的是,装饰函数和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()
      

      【讨论】:

        【解决方案7】:

        假设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

        【讨论】:

          【解决方案8】:
          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)
          

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

          【讨论】:

            【解决方案9】:

            更新 __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
            

            【讨论】:

              【解决方案10】:

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

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

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

              【讨论】:

                【解决方案11】:

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

                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/

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-07-03
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-02-12
                  • 2012-08-18
                  • 1970-01-01
                  • 2011-05-05
                  相关资源
                  最近更新 更多