简体   繁体   English

当参数是递归函数时,修饰器如何工作?

[英]how a decorator works when the argument is recursive function?

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

this is the decorator. 这是装饰工。

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

the parts of result is: 结果的部分是:

[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

how this decorator works when the argument is recursive function? 当参数是递归函数时,此装饰器如何工作? why the decorator can be executed for many times. 为什么装饰器可以执行多次。 how it works? 这个怎么运作?

In your example, the clock decorator is executed once, when it replaces the original version of factorial with the clocked version. 在您的示例中, clock装饰器在将clock factorial替换原始版本的factorial时执行一次。 The original factorial is recursive and therefore the decorated version is recursive too. 原始factorial是递归的,因此修饰后的版本也是递归的。 And so you get the timing data printed for each recursive call - the decorated factorial calls itself, not the original version, because the name factorial now refers to the decorated version. 这样,您就可以为每个递归调用打印时序数据-装饰的factorial调用本身而不是原始版本,因为名称factorial现在是指装饰的版本。


It's a good idea to use functools.wraps in decorators. 在装饰器中使用functools.wraps是一个好主意。 This copies various attributes of the original function to the decorated version. 这会将原始功能的各种属性复制到修饰的版本。

For example, without 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__)

output 输出

clocked  Clocking decoration wrapper 

With wraps : 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__)

output 输出

factorial  Recursive factorial 

which is what we'd get if we did print(factorial.__name__, factorial.__doc__) on the undecorated version. 如果我们在未修饰的版本上print(factorial.__name__, factorial.__doc__) ,将会得到什么。


If you don't want the clock -decorated recursive function to print the timing info for all of the recursive calls, it gets a bit tricky. 如果您不希望clock装饰的递归函数为所有递归调用打印时序信息,则它会有些棘手。

The simplest way is to not use the decorator syntax and just call clock as a normal function so we get a new name for the clocked version of the function: 最简单的方法是不使用装饰器语法,而只是将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)))

output 输出

[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

Another way is to wrap the recursive function in a non-recursive function and apply the decorator to the new function. 另一种方法是将递归函数包装在非递归函数中,并将装饰器应用于新函数。

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)))

output 输出

[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

Another way is to modify the decorator so that it keeps track of whether it's executing the top level of the recursion or one of the inner levels, and only print the timing info for the top level. 另一种方法是修改装饰器,以使其跟踪是执行递归的顶级还是内部级别之一,并且仅打印顶级的计时信息。 This version uses the nonlocal directive so it only works in Python 3, not Python 2. 此版本使用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))) 

output 输出

[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

The rclock function can be used on non-recursive functions, but it's a little more efficient to just use the original version of clock . rclock函数可以用于非递归函数,但是仅使用原始版本的clock更有效率。


Another handy function in functools that you should know about if you're using recursive functions is lru_cache . 如果您正在使用递归函数, functools中应该知道的另一个便捷函数是lru_cache This keeps a cache of recently computed results so they don't need to be re-computed. 这样可以保留最近计算的结果的缓存,因此不需要重新计算它们。 This can enormously speed up recursive functions. 这可以极大地加速递归函数。 Please see the docs for details. 请参阅文档以获取详细信息。

We can use lru_cache in conjunction with clock or rclock . 我们可以将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)))

output 输出

[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

As you can see, even though we used the plain clock decorator only a single line of timing info gets printed for the factorials of 4, 5, and 6 because the smaller factorials are read from the cache instead of being re-computed. 如您所见,即使我们使用普通clock装饰器,也只为4、5和6的阶乘打印了一行定时信息,因为较小的阶乘是从缓存读取的,而不是重新计算的。

When you apply a decorator to a function, the function is passed as a parameter to the decorator. 将装饰器应用于函数时,该函数将作为参数传递给装饰器。 Whether the function is recursive or not does not matter. 该函数是否递归无关紧要。

The code 编码

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

is equivalent to 相当于

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

The decorated function is passed to the decorator as an argument and returned another function to replace the original one, the returned function is no recursive function, but when you call it, it'll call the original recursive function: 装饰函数作为参数传递给装饰器,并返回另一个函数来替换原始函数,返回的函数不是递归函数,但是当您调用它时,它将调用原始递归函数:

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

When you call your decorated function factorial , what you actually called is clocked , and it actually call factorial in the following line: 当您调用修饰的函数factorial ,实际调用的是clocked ,它实际上在以下行中调用factorial

result = func(*args)

The decorator is executed only once. 装饰器仅执行一次。

For understanding, you can think your function becomes to the following one after @clock : 为了理解,您可以认为您的函数在@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

Maybe it helps to assume the "syntactic sugar" point of view. 也许有助于假设“句法糖”的观点。 This is from PEP 318 with modifications (I simplified the example) 这是来自PEP 318的修改(我简化了示例)

The current syntax for function decorators as implemented in Python 2.4a2 is: 在Python 2.4a2中实现的函数装饰器的当前语法为:

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

This is equivalent to: 这等效于:

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

As you can see the decorator function is called only once and the wrapper it returns is assigned to the name of the decorated function. 如您所见,装饰器函数仅被调用一次,并且返回的包装器被分配给装饰函数的名称。

Thus whenever the original function is called by its name (for example in a recursion) the wrapper (but not the decorator function) is called instead. 因此,无论何时以其名称(例如,在递归中)调用原始函数时,都将调用包装器(而不是装饰器函数)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM