参数是递归函数时装饰器如何工作?

时间:2017-04-16 01:53:48

标签: python decorator

import time
def clock(func):
   def clocked(*args):
       t0 = time.perf_counter()
       result = func(*args) 
       elapsed = time.perf_counter() - t0
       name = func.__name__
       arg_str = ', '.join(repr(arg) for arg in args)
       print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
       return result
   return clocked

这是装饰者。

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

结果的部分是:

[0.00000191s] factorial(1) -> 1
[0.00004911s] factorial(2) -> 2
[0.00008488s] factorial(3) -> 6
[0.00013208s] factorial(4) -> 24
[0.00019193s] factorial(5) -> 120
[0.00026107s] factorial(6) -> 720
6! = 720

当参数是递归函数时,这个装饰器是如何工作的?为什么装饰器可以执行多次。它是如何工作的?

4 个答案:

答案 0 :(得分:4)

在您的示例中,clock装饰器执行一次,当它用时钟版本替换factorial的原始版本时。原始factorial是递归的,因此装饰版本也是递归的。因此,您可以获得为每个递归调用打印的时间数据 - 装饰的factorial调用本身,而不是原始版本,因为名称factorial现在指的是装饰版本。

在装饰器中使用functools.wraps是个好主意。这会将原始函数的各种属性复制到装饰版本。

例如,没有wraps

import time

def clock(func):
    def clocked(*args):
        ''' Clocking decoration wrapper '''
        t0 = time.perf_counter()
        result = func(*args) 
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

@clock
def factorial(n):
    ''' Recursive factorial '''
    return 1 if n < 2 else n * factorial(n-1)

print(factorial.__name__, factorial.__doc__)

<强>输出

clocked  Clocking decoration wrapper 

使用wraps

import time
from functools import wraps

def clock(func):
    @wraps(func)
    def clocked(*args):
        ''' Clocking decoration wrapper '''
        t0 = time.perf_counter()
        result = func(*args) 
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

@clock
def factorial(n):
    ''' Recursive factorial '''
    return 1 if n < 2 else n * factorial(n-1)

print(factorial.__name__, factorial.__doc__)

<强>输出

factorial  Recursive factorial 

如果我们在未修饰的版本上print(factorial.__name__, factorial.__doc__),我们就会得到这些内容。

如果您不希望clock - 修饰的递归函数打印所有递归调用的时序信息,则会有点棘手。

最简单的方法是不使用装饰器语法并只调用clock作为普通函数,因此我们为函数的时钟版本获取一个新名称:

def factorial(n):
    return 1 if n < 2 else n * factorial(n-1)

clocked_factorial = clock(factorial)

for n in range(7):
    print('%d! = %d' % (n, clocked_factorial(n)))

<强>输出

[0.00000602s] factorial(0) -> 1
0! = 1
[0.00000302s] factorial(1) -> 1
1! = 1
[0.00000581s] factorial(2) -> 2
2! = 2
[0.00000539s] factorial(3) -> 6
3! = 6
[0.00000651s] factorial(4) -> 24
4! = 24
[0.00000742s] factorial(5) -> 120
5! = 120
[0.00000834s] factorial(6) -> 720
6! = 720

另一种方法是将递归函数包装在非递归函数中,并将装饰器应用于新函数。

def factorial(n):
    return 1 if n < 2 else n * factorial(n-1)

@clock
def nr_factorial(n):
    return factorial(n)

for n in range(3, 7):
    print('%d! = %d' % (n, nr_factorial(n)))

<强>输出

[0.00001018s] nr_factorial(3) -> 6
3! = 6
[0.00000799s] nr_factorial(4) -> 24
4! = 24
[0.00000801s] nr_factorial(5) -> 120
5! = 120
[0.00000916s] nr_factorial(6) -> 720
6! = 720

另一种方法是修改装饰器,以便跟踪它是执行递归的顶层还是其中一个内层,并且只打印顶层的定时信息。此版本使用nonlocal指令,因此它仅适用于Python 3,而不适用于Python 2.

def rclock(func):
    top = True
    @wraps(func)
    def clocked(*args):
        nonlocal top
        if top:
            top = False
            t0 = time.perf_counter()
            result = func(*args) 
            elapsed = time.perf_counter() - t0
            name = func.__name__
            arg_str = ', '.join(repr(arg) for arg in args)
            print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        else:
            result = func(*args)
            top = True
        return result
    return clocked

@rclock
def factorial(n):
    return 1 if n < 2 else n * factorial(n-1)

for n in range(3, 7):
    print('%d! = %d' % (n, factorial(n))) 

<强>输出

[0.00001253s] factorial(3) -> 6
3! = 6
[0.00001205s] factorial(4) -> 24
4! = 24
[0.00001227s] factorial(5) -> 120
5! = 120
[0.00001422s] factorial(6) -> 720
6! = 720

rclock函数可用于非递归函数,但只使用clock的原始版本效率更高。

functools中另一个方便的功能,你应该知道你是否使用递归函数是lru_cache。这保留了最近计算结果的缓存,因此不需要重新计算它们。这可以极大地加速递归函数。有关详细信息,请参阅文档。

我们可以将lru_cacheclockrclock结合使用。

@lru_cache(None)
@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n-1)

for n in range(3, 7):
    print('%d! = %d' % (n, factorial(n)))

<强>输出

[0.00000306s] factorial(1) -> 1
[0.00017850s] factorial(2) -> 2
[0.00022049s] factorial(3) -> 6
3! = 6
[0.00000542s] factorial(4) -> 24
4! = 24
[0.00000417s] factorial(5) -> 120
5! = 120
[0.00000409s] factorial(6) -> 720
6! = 720

正如您所看到的,即使我们使用普通clock装饰器,也只会为4,5和6的因子打印一行时序信息,因为从缓存中读取较小的因子而不是被重新计算。

答案 1 :(得分:1)

将装饰器应用于函数时,该函数作为参数传递给装饰器。函数是否递归并不重要。

代码

@clock
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

相当于

def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)
factorial = clock(factorial)

答案 2 :(得分:0)

装饰函数作为参数传递给装饰器并返回另一个函数来替换原始函数,返回的函数不是递归函数,但是当你调用它时,它会调用原始的递归函数: / p>

def clock(func):
  def clocked(*args):
   t0 = time.perf_counter()
   result = func(*args) # You call your original recursive function here
   elapsed = time.perf_counter() - t0
   name = func.__name__
   arg_str = ', '.join(repr(arg) for arg in args)
   print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
   return result
return clocked

当您调用已修饰的函数factorial时,您实际调用的内容为clocked,它实际上会在以下行中调用factorial

result = func(*args)

装饰器只执行一次。

为了便于理解,您可以在@clock之后认为您的功能属于以下内容:

def factorial(*args):
    def _factorial(n):
        return 1 if n < 2 else n*_factorial(n-1)
    t0 = time.perf_counter()
    result = _factorial(*args)
    elapsed = time.perf_counter() - t0
    name = _factorial.__name__
    arg_str = ', '.join(repr(arg) for arg in args)
    print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
    return result

答案 3 :(得分:0)

也许有必要假设“语法糖”的观点。 这来自PEP 318并进行了修改(我简化了示例)

  

Python 2.4a2中实现的函数装饰器的当前语法是:

@dec
def func(arg1, arg2, ...):
    pass
  

这相当于:

def func(arg1, arg2, ...):
    pass
func = dec(func)

正如您所看到的那样,decorator函数只被调用一次,它返回的包装器被赋值给装饰函数的名称。

因此,无论何时通过名称调用原始函数(例如在递归中),都会调用包装器(而不是装饰器函数)。

相关问题