python的函数装饰器(UCB cs61a)

深入学习装饰器

在学习UCB的cs61a的时候,遇到了一个之前学python没有认真了解过的知识点,因此这回花了一点时间去认真学习它。

Python编程语言有一个有趣的句法功能,称为装饰器。 让我们用一个例子来说明如何以及为什么要使用Python装饰器。这也是CS61a中使用的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def fib(n):
if n in (0, 1):
return n
else:
return fib(n - 1) + fib(n - 2)
>>> fib(0)
0
>>> fib(1)
1
>>> fib(4)
3
>>> fib(10)
55
>>> fib(30)
832040
>>> fib(35)
9227465

经过测试,我们可以发现,当n越大的时候,计算斐波那契数列的时间就越长。原因我们可以用一个树递归来看看计算fib(5)的时候,发生了什么?

image

不难发现,在计算fib(5)的时候,fib(0)计算了三次,fib(1)计算了五次等等。简而言之,就是在计算fib)(5)的时候出现了大量计算。为了使其运行速度更快,我们可以使用一种名为memoization的技术。memoization在缓存中存储与某些特定输入集相对应的结果。 在以后再对其进行调用时,将使用以前存储在缓存中的结果,从而避免重新计算。 这意味着调用具有某些参数的斐波那契数列的主要成本是在第一次调用所花费的,

以下就是我们使用的memorization结果:

1
2
3
4
5
6
7
def memoize(f):
cache = {}
def helper(x):
if x not in cache:
cache[x] = f(x)
return cache[x]
return helper

memoize函数将返回另一个函数,称为helper,helper将为通过参数f接收的函数提供附加功能。helper在创建时将范围内的变量绑定记录了下来,即计算过的斐波那契数列将会被记录在缓存中,下次调用的时候不需要再次计算,而是直接从cache中取出来。

再次调用fib,计算fib(100)的值,可以看到它将迅速计算出结果

1
2
3
>>>fib = memoize(fib)
>>>fib(100)
354224848179261915075

fib = memoize(fib)可以理解成,fib函数被memoize修饰了。在python语法中,有一种简便的使用方式:

1
2
3
4
5
6
7
@some_decorator
def some_function():
# function body...
#等同于
def some_function():
# function body...
some_function = some_decorator(some_function)

因此,本质上,decorator就是一个返回函数的高阶函数。


  • 另一个非常有趣的是,在python装饰器中,我们可以链接多个装饰器在一起。借此,我们来扩展我们的例子来打印我们的调用日志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def memoize(f):
# Same code as before...

def trace(f):
def helper(x):
call_str = "{0}({1})".format(f.__name__, x)
print("Calling {0} ...".format(call_str))
result = f(x)
print("... returning from {0} = {1}".format(
call_str, result))
return result
return helper

@memoize
@trace
def fib(n):
if n in (0, 1):
return n
else:
return fib(n - 1) + fib(n - 2)

>>> fib(5)
Calling fib(5) ...
Calling fib(4) ...
Calling fib(3) ...
Calling fib(2) ...
Calling fib(1) ...
... returning from fib(1) = 1
Calling fib(0) ...
... returning from fib(0) = 0
... returning from fib(2) = 1
... returning from fib(3) = 2
... returning from fib(4) = 3
... returning from fib(5) = 5
5


  • 还有一个,decorator本身也可以传入参数。

在这里,我借用的例子来解释一下。原因是,如果装饰器本身需要参数的话,decorator也会变得复杂。因此,我写了一个装饰器,既支持带参数的@log,也支持不带参数的@log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functools
def log2(*args):
text = args[0] if isinstance(args[0],str) else 'without_var'
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s, before calling %s():' % (text, func.__name__))
result = func(*args, **kw)
print('%s, after calling %s():' % (text, func.__name__))
return result
return wrapper
return decorator if isinstance(args[0],str) else decorator(args[0])

@log2
def now1():
print('2017-08-09')

@log2('with_var')
def now2():
print('2017-08-09')

"""
>>> now1()
>>> now2()
without_var, before calling now1():
2017-08-09
without_var, after calling now1():
with_var, before calling now2():
2017-08-09
with_var, after calling now2():
"""

不难发现,带参数的装饰器,实则是一个三层嵌套的装饰器。因此调用本质是这样的:

1
2
>>> now2 = log('with_var')(now2)
>>> now1 = log(now1)

解释一下:

如果是带参数的log,如上所示,首先log('with_var')返回了decorator函数,再将now2传进去,返回的则是wrapper函数;

如果是不带参数的log,log('without_var')返回的decorator(args[0]),也就是wrapper函数,此时args[0]是now1函数。

至于,为什么在wrapper前使用@functools.wraps(func),是因为,函数作为一个对象,它的__name__属性在被装饰器修饰过之后,已经从now2和now1都变为了wrapper。@functools.wraps(func)的功能就是还原这一个属性,避免依赖函数签名的代码在执行时出错。

总结

decorator对于扩展函数的功能非常有用,虽然有时在定义的时候非常复杂,需要对高阶函数有一定的熟悉,但当你真正使用起来会发现其具有非常好的便利性。Python直接从语法层次支持decorator,这也是我对python极其喜爱的一大原因。

参考资料


http://programmingbits.pythonblogs.com/27_programmingbits/archive/50_function_decorators.html

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014318435599930270c0381a3b44db991cd6d858064ac0000#0

http://composingprograms.com/pages/17-recursive-functions.html