本章的notebook文件在这里

函数装饰器与闭包

装饰器基础知识   

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数). 装饰器可能会处理被装饰的函数并返回, 或者将其替换为另一个函数或者可调用对象.

例如,下面两段代码是等价的:

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
@decorate 
def target():
print('running target()')
```
---分---割---线-----
```py
def target():
print('runing target()')

target = decorate(target)
```

我们实现一个简单的装饰器来说明一下:

```python
def deco(func):
def youarepig():
print('你是猪')
return youarepig

@deco
def target():
print('哈哈哈')

target()

你是猪

可以发现, 调用target函数后, 打印出来的并不是’哈哈哈’, 而是’你是猪’, 这就是因为deco装饰器在中途掉包了这个函数.
当然装饰器是一种语法糖,但是它确实很好用, 其特性有二:

  • 能把被装饰的函数替换为其他函数
  • 装饰器在加载模块时立即执行

所谓装饰器在加载时立即执行是指, 装饰器本身会被执行, 你用该装饰器装饰了几个函数就会被执行几次, 但是需要注意的是,被装饰的函数本身并不会被执行.

利用装饰器改进策略模式

下面我们装饰器改进一下之前提到的电商促销折扣的代码, 这里的一个问题是,我们每次新增折扣策略的时候,可能会忘记将它加入策略列表中而造成错误, 利用装饰器可以这么写:

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
# BEGIN STRATEGY_BEST4

promos = [] # <1>

def promotion(promo_func): # <2>
promos.append(promo_func)
return promo_func

@promotion # <3>
def fidelity(order):
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item(order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount

@promotion
def large_order(order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0

def best_promo(order): # <4>
"""Select best discount available
"""
return max(promo(order) for promo in promos)

我们定义一个会将传入的函数加入策略列表的装饰器, 并用该装饰器去装饰每个定义的策略.

当然, 通常我们使用装饰器时会改变传入的函数, 而不是仅仅将它原样返回, 为此我们需要先了解闭包和python的变量作用域.

变量作用域规则   

python并不要求声明变量, 但是他会假定在函数定义体中的复制变量为局部变量. 即时之前定义了一个同名的全局变量, python解释器也会认为其为局部变量, 例如:

1
2
3
4
5
6
7
8
b = 6

def f(a):
print(a)
print(b)
b = 9

f(3)
3
---------------------------------------------------------------------------

UnboundLocalError                         Traceback (most recent call last)

<ipython-input-3-7bc160830b6f> in <module>()
      6     b = 9
      7 
----> 8 f(3)


<ipython-input-3-7bc160830b6f> in f(a)
      3 def f(a):
      4     print(a)
----> 5     print(b)
      6     b = 9
      7 

UnboundLocalError: local variable 'b' referenced before assignment

上述代码中在print(b)的时候就会出错, 虽然之前已经给b赋过值, 但是由于bf函数内出现过, 因此会被认为是局部变量, 要解决这个问题, 需要在函数f内部将b声明为global.

1
2
3
4
5
6
7
8
9
b = 6

def f(a):
global b
print(a)
print(b)
b = 9

f(3)
3
6

闭包

闭包在python中指的是延伸了作用域的函数, 其中包含函数定义体中引用,但是不在定义体中定义的非全局变量.
听上去很拗口, 具体举个例子就容易明白了.下面定义一个高阶函数来计算序列的平均值.

1
2
3
4
5
6
7
8
9
10
11
def make_average():
series = []

def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)

return averager
avg = make_average()
avg(10)
10.0
1
avg(11)
10.5
1
avg(12)
11.0

那么很好奇一点,当make_average返回averager函数后, 我们序列中的历史值是存在哪儿的呢?
观察averager函数中, series于它是一个自有变量, 即没有绑定在本地作用域的变量, 但是我们发现每次avg的时候它都能访问到该变量,我们审查一下该变量:

1
avg.__code__.co_varnames
('new_value', 'total')
1
avg.__code__.co_freevars
('series',)
1
avg.__closure__
(<cell at 0x7f803f933dc8: list object at 0x7f803e859f88>,)
1
avg.__closure__[0].cell_contents
[10, 11, 12]

可以发现, series以自由变量的形式保存在__closure__属性中. avg.__closure__的各个元素对应于avg.__code__.co_freevars中的一个名称, 这些元素是cell对象, 有一个cell_contents对象其中保存着真实的值.

现在回头看这个averager, 可以发现我们其实只用到了这些数的和和计数, 而不用整个数值列表, 很自然想到做以下修改:

1
2
3
4
5
6
7
8
9
10
11
def make_average():
count = 0
total = 0

def averager(new_value):
count += 1
total += new_value
return total / count

return averager

但是这么写会有有问题, 原因在于, averager里对count赋值了,这会将其变成一个局部变量, 而不再是自由变量, 此时我们需要将其加上一个nonlocal声明.

1
2
3
4
5
6
7
8
9
10
11
12
13
def make_average():
count = 0
total = 0

def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count

return averager
avg(10)
avg(11)
10.8

标准库中的装饰器

下面介绍一下两个标准库中的装饰器, functools.lru_cachesingledispatch

使用functools.lru_cache 做备忘

functools.lru_cache可以用来实现备忘功能,它将耗时的函数的结果保存起来,避免传入相同的参数时重复计算.
LRU即为”Least Recently Used”, 表示会保存最近的缓存, 一段时间不用后的缓存则会被扔掉.

一个适用的场景是递归函数, 例如斐波那契数列的计算. 计算斐波那契数列时, 假设计算f(n), 我们需要先计算f(n-1)和f(n-2), 递推下去其实有很多项是重复计算的, 使用lru_cache就会将中间函数的结算结果缓存, 这样每一项都会只计算一次.
具体代码如下:

1
2
3
4
5
6
import functools
@functools.lru_cache()
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)

单分派泛函数 singledispatch

在写代码时,我们常常会遇到这样的需求,即函数处理的方法根据传入的参数类型不同而有所不同,例如构想一个打印函数传入的如果是:

  • 字符串 str 直接打印
  • 整数 int 以十六进制打印
  • 日期对象 Date 以’MM-DD in YYYY’的格式打印

python不支持重载函数,而使用一连串的if/else来判断再调用相应的函数又显得太过笨拙了, 此时我们可用singledispatch来处理该问题.我们用该装饰器将整体方案拆分成多个模块,并根据不同的参数类型来执行同一组函数. 被装饰的函数叫做泛函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import singledispatch
import numbers
import datetime

@singledispatch
def special_print(obj):
content = obj
return "打印出来是: {}".format(content)

@special_print.register(numbers.Integral)
def _(n):
return "{0} (0x{0:x})".format(n)

@special_print.register(datetime.date)
def _(date):
return date.strftime('%m-%d in %Y')
1
2
# 字符串 按照默认的分派
special_print('哈哈哈啊')
'打印出来是: 哈哈哈啊'
1
2
# 整数 
special_print(1024)
'1024 (0x400)'
1
2
3
# date对象 
d = datetime.date(2018, 11, 11)
special_print(d)
'11-11 in 2018'
1
2
# list 以及其他未被注册分派的参数类型都会按照泛函数默认方式执行
special_print(['1'])
"打印出来是: ['1']"

值得提一句的是, 这里的装饰器是可以叠放的, 如上例中, 我们要以处理整数的方式同样处理浮点数, 则可以在函数上叠放两个装饰器.
@d1, @d2两个装饰器按顺序应用到f上,作用相当于 f=d1(d2(f))

参数化装饰器

有时我们需要给装饰器传入某个参数, 此时则先创建一个装饰器工厂函数, 将参数传给它再返回一个装饰器.
举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
registry = set() 

def register(active=True): # 装饰器工厂函数 接受一个参数
def decorate(func): # 真正的装饰器
print('running register(active=%s)->decorate(%s)'
% (active, func))
if active: # 为真时加入registry
registry.add(func)
else:
registry.discard(func) # 否则删除

return func # 返回函数
return decorate # 返回装饰器

@register(active=False) #
def f1():
print('running f1()')

@register()
def f2():
print('running f2()')

def f3():
print('running f3()')
running register(active=False)->decorate(<function f1 at 0x7f803e7dd950>)
running register(active=True)->decorate(<function f2 at 0x7f803e7ddbf8>)
1
2
3
# 运行f1 程序不会将f1加入registry,因为active为False
f1()
registry
running f1()
{<function __main__.f2>}
1
2
f2()
register
running f2()
<function __main__.register>