【问题标题】:toggling decorators切换装饰器
【发布时间】:2013-01-16 04:16:33
【问题描述】:

打开和关闭装饰器的最佳方式是什么,而无需实际访问每个装饰并将其注释掉?假设你有一个基准装饰器:

# deco.py
def benchmark(func):
  def decorator():
    # fancy benchmarking 
  return decorator

在你的模块中是这样的:

# mymodule.py
from deco import benchmark

class foo(object):
  @benchmark
  def f():
    # code

  @benchmark
  def g():
    # more code

这很好,但有时您不关心基准并且不想要开销。我一直在做以下事情。添加另一个装饰器:

# anothermodule.py
def noop(func):
  # do nothing, just return the original function
  return func

然后将导入行注释掉,再添加一条:

# mymodule.py
#from deco import benchmark 
from anothermodule import noop as benchmark

现在,基准测试是在每个文件的基础上切换的,只需更改相关模块中的导入语句。可以独立控制各个装饰器。

有没有更好的方法来做到这一点?完全不必编辑源文件并指定在其他文件中使用哪些装饰器会很好。

【问题讨论】:

    标签: python decorator


    【解决方案1】:

    您可以将条件添加到装饰器本身:

    def benchmark(func):
        if not <config.use_benchmark>:
            return func
        def decorator():
        # fancy benchmarking 
        return decorator
    

    【讨论】:

    • 这不允许每个文件切换,如果装饰器来自标准库或无法编辑,则必须在每个装饰器的包装器中完成。
    【解决方案2】:

    我一直在使用以下方法。它与 CaptainMurphy 建议的几乎相同,但它的优点是您不需要像调用函数一样调用装饰器。

    import functools
    
    class SwitchedDecorator:
        def __init__(self, enabled_func):
            self._enabled = False
            self._enabled_func = enabled_func
    
        @property
        def enabled(self):
            return self._enabled
    
        @enabled.setter
        def enabled(self, new_value):
            if not isinstance(new_value, bool):
                raise ValueError("enabled can only be set to a boolean value")
            self._enabled = new_value
    
        def __call__(self, target):
            if self._enabled:
                return self._enabled_func(target)
            return target
    
    
    def deco_func(target):
        """This is the actual decorator function.  It's written just like any other decorator."""
        def g(*args,**kwargs):
            print("your function has been wrapped")
            return target(*args,**kwargs)
        functools.update_wrapper(g, target)
        return g
    
    
    # This is where we wrap our decorator in the SwitchedDecorator class.
    my_decorator = SwitchedDecorator(deco_func)
    
    # Now my_decorator functions just like the deco_func decorator,
    # EXCEPT that we can turn it on and off.
    my_decorator.enabled=True
    
    @my_decorator
    def example1():
        print("example1 function")
    
    # we'll now disable my_decorator.  Any subsequent uses will not
    # actually decorate the target function.
    my_decorator.enabled=False
    @my_decorator
    def example2():
        print("example2 function")
    

    在上面,example1 将被装饰,而 example2 将不会被装饰。当我必须按模块启用或禁用装饰器时,我只有一个函数,可以在需要不同副本时创建一个新的 SwitchedDecorator。

    【讨论】:

    • 这是否适用于接受参数的装饰器?
    【解决方案3】:

    我认为你应该使用装饰器 a 来装饰装饰器 b,它可以让你在决策函数的帮助下打开或关闭装饰器 b。

    这听起来很复杂,但想法却很简单。

    假设你有一个装饰器记录器:

    from functools import wraps 
    def logger(f):
        @wraps(f)
        def innerdecorator(*args, **kwargs):
            print (args, kwargs)
            res = f(*args, **kwargs)
            print res
            return res
        return innerdecorator
    

    这是一个非常无聊的装饰器,我有十几个这样的,缓存器,记录器,注入东西的东西,基准测试等。我可以使用 if 语句轻松扩展它,但这似乎是一个糟糕的选择;因为那我得换十几个装饰器,一点都不好玩。

    那该怎么办?让我们更上一层楼。假设我们有一个装饰器,它可以装饰一个装饰器?这个装饰器看起来像这样:

    @point_cut_decorator(logger)
    def my_oddly_behaving_function
    

    这个装饰器接受记录器,这不是一个很有趣的事实。但它也有足够的权力来选择是否应该将记录器应用于 my_oddly_behave_function。我称它为 point_cut_decorator,因为它具有面向方面编程的某些方面。切入点是一组位置,其中一些代码(建议)必须与执行流程交织在一起。切点的定义通常在一个地方。这种技术似乎非常相似。

    我们如何实现它的决策逻辑。好吧,我选择了一个函数,它接受被装饰者、装饰者、文件名称,它只能说明是否应该应用装饰器。这些是坐标,足以非常精确地确定位置。

    这是 point_cut_decorator 的实现,我选择将决策函数实现为一个简单的函数,你可以扩展它以让它根据你的设置或配置来决定,如果你对所有 4 个坐标使用正则表达式,你最终会具有非常强大的功能:

    from functools import wraps
    

    myselector 是决策函数,如果为 true,则应用装饰器,如果为 false,则不应用。参数是文件名、模块名、装饰对象,最后是装饰器。这使我们能够以细粒度的方式切换行为。

    def myselector(fname, name, decoratee, decorator):
        print fname
    
        if decoratee.__name__ == "test" and fname == "decorated.py" and decorator.__name__ == "logger":
            return True
        return False 
    

    这会装饰一个函数,检查 myselector,如果 myselector 说继续,它会将装饰器应用于函数。

    def point_cut_decorator(d):
        def innerdecorator(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                if myselector(__file__, __name__, f, d):
                    ps = d(f)
                    return ps(*args, **kwargs)
                else:
                    return f(*args, **kwargs)
            return wrapper
        return innerdecorator
    
    
    def logger(f):
        @wraps(f)
        def innerdecorator(*args, **kwargs):
            print (args, kwargs)
            res = f(*args, **kwargs)
            print res
            return res
        return innerdecorator
    

    这就是你使用它的方式:

    @point_cut_decorator(logger)
    def test(a):
        print "hello"
        return "world"
    
    test(1)
    

    编辑:

    这就是我讲的正则表达式方法:

    from functools import wraps
    import re
    

    如您所见,我可以在某处指定一些规则,这些规则决定是否应用装饰器:

    rules = [{
        "file": "decorated.py",
        "module": ".*",
        "decoratee": ".*test.*",
        "decorator": "logger"
    }]
    

    然后我遍历所有规则,如果规则匹配则返回 True,如果规则不匹配则返回 false。通过在生产中使规则为空,这不会使您的应用程序太慢:

    def myselector(fname, name, decoratee, decorator):
        for rule in rules:
            file_rule, module_rule, decoratee_rule, decorator_rule = rule["file"], rule["module"], rule["decoratee"], rule["decorator"]
            if (
                re.match(file_rule, fname)
                and re.match(module_rule, name)
                and re.match(decoratee_rule, decoratee.__name__)
                and re.match(decorator_rule, decorator.__name__)
            ):
                return True
        return False
    

    【讨论】:

      【解决方案4】:

      这是我最终想出的每个模块切换的方法。它以@nneonneo 的建议为起点。

      随机模块正常使用装饰器,不知道切换。

      foopkg.py:

      from toggledeco import benchmark
      
      @benchmark
      def foo():
          print("function in foopkg")
      

      barpkg.py:

      from toggledeco import benchmark
      
      @benchmark
      def bar():
          print("function in barpkg")
      

      装饰器模块本身为所有被禁用的装饰器维护了一组函数引用,每个装饰器检查它是否存在于这个集合中。如果是这样,它只返回原始函数(没有装饰器)。默认情况下,该集合为空(一切都已启用)。

      toggledeco.py:

      import functools
      
      _disabled = set()
      def disable(func):
          _disabled.add(func)
      def enable(func):
          _disabled.discard(func)
      
      def benchmark(func):
          if benchmark in _disabled:
              return func
          @functools.wraps(func)
          def deco(*args,**kwargs):
              print("--> benchmarking %s(%s,%s)" % (func.__name__,args,kwargs))
              ret = func(*args,**kwargs)
              print("<-- done")
          return deco
      

      主程序可以在导入期间打开和关闭各个装饰器:

      from toggledeco import benchmark, disable, enable
      
      disable(benchmark) # no benchmarks...
      import foopkg
      
      enable(benchmark) # until they are enabled again
      import barpkg
      
      foopkg.foo() # no benchmarking 
      barpkg.bar() # yes benchmarking
      
      reload(foopkg)
      foopkg.foo() # now with benchmarking
      

      输出:

      function in foopkg
      --> benchmarking bar((),{})
      function in barpkg
      <-- done
      --> benchmarking foo((),{})
      function in foopkg
      <-- done
      

      这增加了错误/功能,启用/禁用将渗透到从主函数中导入的模块导入的任何子模块。

      编辑

      这是@nneonneo 建议的课程。为了使用它,装饰器必须作为函数调用(@benchmark(),而不是@benchmark)。

      class benchmark:
          disabled = False
      
          @classmethod
          def enable(cls):
              cls.disabled = False
      
          @classmethod
          def disable(cls):
              cls.disabled = True
      
          def __call__(cls,func):
              if cls.disabled:
                  return func
              @functools.wraps(func)
              def deco(*args,**kwargs):
                  print("--> benchmarking %s(%s,%s)" % (func.__name__,args,kwargs))
                  ret = func(*args,**kwargs)
                  print("<-- done")
              return deco
      

      【讨论】:

      • 有趣的方法。我会更进一步,使用enable()disable() 方法使装饰器成为一个完整的类。 __call__() 方法允许它表现得像一个装饰器,尽管它带有状态。这将摆脱 enabledisable 函数以及设置全局 _disabled 的需要,因为每个单独的装饰器都将保持该状态。
      【解决方案5】:

      我会在装饰器的主体中检查配置文件。如果必须根据配置文件使用基准测试,那么我会转到您当前的装饰器主体。如果没有,我将返回该函数并且什么也不做。这种味道的东西:

      # deco.py
      def benchmark(func):
        if config == 'dontUseDecorators': # no use of decorator
            # do nothing
            return func
        def decorator(): # else call decorator
            # fancy benchmarking 
        return decorator
      

      调用装饰函数时会发生什么? @

      @benchmark
      def f():
          # body comes here
      

      是语法糖

      f = benchmark(f)
      

      所以如果 config 想让你忽略装饰器,你只是在做 f = f() 这是你所期望的。

      【讨论】:

        【解决方案6】:

        我认为还没有人提出这个建议:

        benchmark_modules = set('mod1', 'mod2') # Load this from a config file
        
        def benchmark(func):
          if not func.__module__ in benchmark_modules:
              return func
        
          def decorator():
            # fancy benchmarking 
          return decorator
        

        每个函数或方法都有一个__module__ 属性,它是定义函数的模块的名称。创建要进行基准测试的模块的白名单(或黑名单,如果您愿意),如果您不想对该模块进行基准测试,只需返回原始未修饰的函数。

        【讨论】:

          【解决方案7】:

          另一种直接方式:

          # mymodule.py
          from deco import benchmark
          
          class foo(object):
          
            def f():
              # code
          
            if <config.use_benchmark>:
              f = benchmark(f)
          
            def g():
              # more code
          
            if <config.use_benchmark>:
              g = benchmark(g)
          

          【讨论】:

            猜你喜欢
            • 2019-04-25
            • 1970-01-01
            • 2017-07-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-11-24
            • 2021-10-22
            • 2020-12-16
            相关资源
            最近更新 更多