【问题标题】:How are these type of python decorators written?这些类型的 python 装饰器是如何编写的?
【发布时间】:2009-07-09 20:16:56
【问题描述】:

我想写一个装饰器来限制函数可以执行的次数,语法如下:


@max_execs(5)
def my_method(*a,**k):
   # do something here
   pass

我认为可以编写这种类型的装饰器,但我不知道如何。我认为一个函数不会是这个装饰器的第一个参数,对吧?我想要一个“简单的装饰器”实现,而不是带有 call 方法的类。

这样做的原因是要了解它们的编写方式。请解释语法,以及装饰器的工作原理。

【问题讨论】:

    标签: python language-features decorator


    【解决方案1】:

    这就是我提出来的。它不使用类,但确实使用函数属性:

    def max_execs(n=5):
        def decorator(fn):
            fn.max = n
            fn.called = 0
            def wrapped(*args, **kwargs):
                fn.called += 1
                if fn.called <= fn.max:
                    return fn(*args, **kwargs)
                else:
                    # Replace with your own exception, or something
                    # else that you want to happen when the limit
                    # is reached
                    raise RuntimeError("max executions exceeded")
            return wrapped
        return decorator
    

    max_execs 返回一个名为decorator 的函数,它又返回wrappeddecoration 将最大执行次数和当前执行次数存储在两个函数属性中,然后在 wrapped 中进行检查。

    译文:这样使用装饰器时:

    @max_execs(5)
    def f():
        print "hi!"
    

    你基本上是在做这样的事情:

    f = max_execs(5)(f)
    

    【讨论】:

    • 在应用这个装饰器时,代码是如何被python“翻译”出来的?例如,如果我的方法被称为 blabla,并且我应用了 max_execs 属性,那么 Python 将如何看待它? blabla = max_execs(5)(blabla) ?
    • 我更新了答案以包含翻译,但你的想法是正确的。
    【解决方案2】:

    装饰器只是将函数转换为其他东西的可调用对象。在您的情况下,max_execs(5) 必须是将函数转换为另一个可调用对象的可调用对象,该对象将计数并转发调用。

    class helper:
        def __init__(self, i, fn):
            self.i = i
            self.fn = fn
        def __call__(self, *args, **kwargs):
            if self.i > 0:
                self.i = self.i - 1
                return self.fn(*args, **kwargs)
    
    class max_execs:
        def __init__(self, i):
            self.i = i
        def __call__(self, fn):
            return helper(self.i, fn)
    

    我不明白您为什么要将自己限制为一个函数(而不是一个类)。但如果你真的想...

    def max_execs(n):
        return lambda fn, i=n: return helper(i, fn)
    

    【讨论】:

      【解决方案3】:

      有两种方法。面向对象的方式是做一个类:

      class max_execs:
          def __init__(self, max_executions):
              self.max_executions = max_executions
              self.executions = 0
      
          def __call__(self, func):
              @wraps(func)
              def maybe(*args, **kwargs):
                  if self.executions < self.max_executions:
                      self.executions += 1
                      return func(*args, **kwargs)
                  else:
                      print "fail"
              return maybe
      

      有关wraps 的解释,请参阅this question

      对于这种装饰器,我更喜欢上述 OOP 方法,因为您基本上有一个私有计数变量来跟踪执行次数。但是,另一种方法是使用闭包,例如

      def max_execs(max_executions):
          executions = [0]
          def actual_decorator(func):
              @wraps(func)
              def maybe(*args, **kwargs):
                  if executions[0] < max_executions:
                      executions[0] += 1
                      return func(*args, **kwargs)
                  else:
                      print "fail"
              return maybe
          return actual_decorator
      

      这涉及三个功能。 max_execs 函数被赋予一个执行次数的参数,并返回一个装饰器,它将限制你进行那么多调用。该函数 actual_decorator 与 OOP 示例中的 __call__ 方法执行相同的操作。唯一奇怪的是,由于我们没有带有私有变量的类,因此我们需要改变闭包外部范围内的 executions 变量。 Python 3.0 通过 nonlocal 语句支持这一点,但在 Python 2.6 或更早版本中,我们需要将执行计数包装在一个列表中,以便它可以被变异。

      【讨论】:

      • 如果例如一个带有 @logged 属性的方法意味着 method = logged(method) ,那么带有 @max_execs(5) 的方法如何“翻译”?
      • 跟说max_execs(5)(f)是一样的
      【解决方案4】:

      不依赖于类中的状态,您必须将状态(计数)保存在函数本身中:

      def max_execs(count):
          def new_meth(meth):
              meth.count = count
              def new(*a,**k):
                  meth.count -= 1
                  print meth.count            
                  if meth.count>=0:
                      return meth(*a,**k)
              return new
          return new_meth
      
      @max_execs(5)
      def f():
          print "invoked"
      
      [f() for _ in range(10)]
      

      它给出:

      5
      invoked
      4
      invoked
      3
      invoked
      2
      invoked
      1
      invoked
      0
      -1
      -2
      -3
      -4
      

      【讨论】:

      • 糟糕,使用类似解决方案的 mipadi 速度更快。
      【解决方案5】:

      此方法不修改函数内部结构,而是将其包装到可调用对象中。

      与使用修补函数相比,使用类会使执行速度减慢约 20%!

      def max_execs(n=1):
          class limit_wrapper:
              def __init__(self, fn, max):
                  self.calls_left = max
                  self.fn = fn
              def __call__(self,*a,**kw):
                  if self.calls_left > 0:
                      self.calls_left -= 1
                      return self.fn(*a,**kw)
                  raise Exception("max num of calls is %d" % self.i)
      
      
          def decorator(fn):
              return limit_wrapper(fn,n)
      
          return decorator
      
      @max_execs(2)
      def fun():
          print "called"
      

      【讨论】:

        【解决方案6】:

        我知道你说过你不想上课,但不幸的是,这是我能想到的唯一方法。

        class mymethodwrapper:
            def __init__(self):
                self.maxcalls = 0
            def mymethod(self):
                self.maxcalls += 1
                if self.maxcalls > 5:
                    return
                #rest of your code
                print "Code fired!"
        

        像这样点燃它

        a = mymethodwrapper
        for x in range(1000):
            a.mymethod()
        

        输出将是:

        >>> Code fired!
        >>> Code fired!
        >>> Code fired!
        >>> Code fired!
        >>> Code fired!
        

        【讨论】:

        • this 不能与任何方法一起使用,并且使用可调用对象。我想要一个普通的装饰器。
        猜你喜欢
        • 2012-10-28
        • 2011-12-26
        • 2021-01-28
        • 1970-01-01
        • 2011-07-25
        • 1970-01-01
        • 2016-01-24
        • 2015-03-24
        • 2015-03-24
        相关资源
        最近更新 更多