装饰器和闭包

装饰器用来在不修改原函数代码的情况下,增强该函数的功能。要想熟练掌握装饰器,必须理解闭包。

闭包粗略的讲就是保存有状态的函数,它除了在装饰器中有作用之外,还是回调式异步编程和函数式编程的基础。所以闭包的重要性可想而知。

装饰器基础知识

装饰器就本质而言依然是一个函数,他接收一个函数作为参数,然后返回它,或者返回另一个函数。

 1 import functools
 2 def deco(func):
 3     #@functools.wraps(func) 
 4     def inner():
 5         print("running inner()")
 6     return inner
 7 
 8 @deco
 9 def target():
10     print("running target()")
11 """
12 target = deco(target) 与
13 @deco
14 def target():
15     ...
16 是一样的,@只是python的语法糖而已
17 """
18 target()
19 print(target)

使用python的@语法,虽然函数名称还是target,但是函数指针已经发生了改变,不再指向自身,而是指向deco里面的inner函数。

print(target)打印的结果是:<function deco.<locals>.inner at 0x0000000003590378>

@functools.wraps(func)的作用是把inner的函数名__name__替换成target

加上之后print(target)打印的结果就是:<function target at 0x0000000003590378>.

使用装饰器改进策略模式

 1 promos = []
 2 
 3 #装饰器
 4 def deco(func):
 5     promos.append(func)
 6     return func
 7 
 8 @deco
 9 def fidelity_promotion(order):
10     """
11     为积分1000及以上的顾客提供5%的折扣
12     """
13     return order.total()*0.05 if order.customer.fidelity >= 1000 else 0
14 
15 @deco
16 def bulkitem_promotion(order):
17     """
18     单个商品为20个及以上提供10%的折扣
19     """
20     discount = 0
21     for item in order.cart:
22         if item.quantity >= 20:
23             discount += item.total()*0.1
24     return discount
25 
26 @deco
27 def largeorder_promotion(order):
28     """
29     订单中的不同商品达到10个及以上时提供7%折扣
30     """
31     itemset = set(item.product for item in order.cart)
32     #print(itemset)
33     return order.total()*0.07 if len(itemset) >= 10 else 0
34 
35 #最佳的折扣方案
36 def best_promotion(order):
37     return max(promo(order) for promo in promos)
38 
39 print("------------------------")
40 joe = Customer('John Doe', 0)
41 ann = Customer('Ann Smith', 1100)
42 cart = [
43     LineItem('banana', 4, .5),
44     LineItem('apple', 10, 1.5),
45     LineItem('watermellon', 5, 5.0)
46 ]
47 
48 print(Order(joe, cart, best_promotion))
49 print(Order(ann, cart, best_promotion))

使用装饰器修饰几个promotion函数,装饰器接收函数作为参数,然后把函数名追加到promos列表中,在把袁术原封不动的返回,这样做有几个好处。

* 折扣函数可以随意定义,只有在定义前加上@deco即可加入促销策略

* 方便禁用某种策略,只需注释掉@deco

不过,多数装饰器都是会被装饰的函数。通常,装饰器会在内部定义一个内部函数,然后将他返回,替换原来的函数。使用内部函数的代码几乎都要靠闭包才能正确运行,为了理解闭包,我们先来看一下python的变量作用域。

变量的作用域

先来看一个例子

1 def f(x):
2     print(x)
3     print(y)
4 
5 f(3)

毫无疑问,程序会抛出NameError: global name 'y' is not defined。出现错误不奇怪,因为我们没给b赋值。

如果先给全局变量y赋值,在调用f就不会出现任何问题。

1 y = 4
2 def f(x):
3     print(x)
4     print(y)
5 
6 f(3)

以为懂了?再来看一个可能会让你吃惊的例子。

1 y = 4
2 def f(x):
3     print(x)
4     print(y)
5     y += 1
6     
7 f(3)

运行结果表明,程序会抛出NameError: global name 'y' is not defined。一开始我也觉得很吃惊,我本以为程序会引用全局变量y,打印出4,可事实却不是这样。

python解释器会认为y是局部变量,因为 y+=1,也就相当于y=y+1,给y赋值了,那么python就会在局部作用域找y,发现y没有值,因此会报错。如果想要使用全局变量y,可以使用global关键字。

1 y = 4
2 def f(x):
3     print(x)
4         global y
5     print(y)
6     y += 1
7     
8 f(3)
9 print(y) #y的值变为5

闭包

有时,人们会把闭包和函数弄混,这是有历史原因的:在函数内部定义函数的做法并不常见,直到开始使用lambda函数时才会这样做。而且,只有在涉及嵌套函数才会有闭包问题,因此许多人是同时知道这两个概念的。

闭包,也就是延伸了作用域的函数,包括函数体中的引用、不在函数体也不是全局变量的引用。关键是他能访问函数体之外的非全局变量。

学术性的话通常言简意赅、清晰准确,但不容易理解。

假设我们现在有一个avg函数,他要计算平均值,每次都有新的值加进来,因此每次的平均值可能都不一样。我们想,所以得保存之前的历史数据。

用类实现的代码如下:

 1 class Average(object):
 2     def __init__(self):
 3         self.series = []
 4 
 5     def __call__(self, val):
 6         self.series.append(val)
 7         return sum(self.series)/len(self.series)
 8 
 9 avg = Average()
10 print(avg(1))
11 print(avg(2))
12 print(avg(3))
13 print(avg(4))

但是今天我们要学习的是闭包,所以用高级函数实现如下:

 1 def make_average():
 2     series = []
 3     def average(val):
 4         series.append(val)
 5         return sum(series)/len(series)
 6     return average
 7 
 8 
 9 avg = make_average()
10 print(avg(1))
11 print(avg(2))
12 print(avg(3))
13 print(avg(4))

函数make_average内部定义了一个空列表series,和一个内部函数average,average接收一个参数,然后把参数放进列表中,计算平均值,make_average最后返回average函数。

现在,我们来比较一下两者实现的差别。

类的方式实现很明显,他把所有的历史数据都放在实例的series列表中,但是高阶函数里的average到哪里去找series列表呢?

注意,series定义在make_average函数中,那么在average函数中,series被称为自由变量(free variable)。指的是未在本地作用域中绑定的变量。

series参数被绑定在average函数的__closure__属性中,它是个列表,每个元素是cell对象,数据保存在cell_contents中。

1 avg = make_average()
2 print(avg(1))
3 print(avg(2))
4 print(avg(3))
5 print(avg(4))
6 print(avg.__closure__)
7 for item in avg.__closure__:
8     print(item.cell_contents)

运行结果如下:

综上,闭包本质就是函数,只是他会保留定义时在函数外部存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用,但是保留的那些自由变量依然可以使用。

nonlocal声明

前面的make_average实现效率并不高,因为每次添加一个值进去,都得重新编列一遍series列表求和,如果我们能绑定历史的求和值跟元素个数,显然效率更高。

方法一:

 1 def make_average():
 2     s = 0
 3     n = 0
 4     def average(val):
 5         n += 1
 6         s += val
 7         return s/n
 8     return average
 9 
10 avg = make_average()
11 avg(1)

一运行,咦?怎么报错了呢

别急,我们换种方法试一下:

方法二:

 1 def make_average():
 2     s_n = {'sum':0, 'n':0}
 3     def average(val):
 4         s_n['n'] += 1
 5         s_n['sum'] += val
 6         return s_n['sum']/s_n['n']
 7     return average
 8 
 9 avg = make_average()
10 print(avg(1))
11 print(avg(2))
12 print(avg(3))
13 print(avg(4))

一运行,终于没问题了。这是为什么?

在一开始,我们讨论过变量作用域的话题,方法一中, s+=val等价于 s = s+val,也就是给变量s赋值,这时,python就不再把s当成自由变量,而是当成局部变量看待,因此会报错,告诉你s是局部变量在没有赋值前被引用。再来看下为什么方法二没问题,方法二定义了一个字典,尽管在average函数里修改字典里键对应的值,但是并没有对字典本身做什么赋值操作,因此s_n绑定的还是自由变量,同理,你换成列表用append的方式也是一样的。

为了解决方法一中存在的问题,python3引入了nonlocal声明。它的作用就是把变量标记为自由变量,明确的告诉python解释器,这个变量是自由变量而非局部变量,即使在给它赋值,那也是自由变量。

正确的代码如下:

 1 def make_average():
 2     s, n = 0, 0
 3     def average(val):
 4         nonlocal s, n #标记为自由变量
 5         s += val
 6         n += 1
 7         return s/n
 8     return average
 9 
10 
11 avg = make_average()
12 print(avg(1))
13 print(avg(2))
14 print(avg(3))
15 print(avg(4))

 

 

posted on 2019-03-05 10:52  forwardFields  阅读(868)  评论(0编辑  收藏  举报

导航