博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
装饰器和闭包
阅读量:4668 次
发布时间:2019-06-09

本文共 5432 字,大约阅读时间需要 18 分钟。

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

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

装饰器基础知识

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

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 @deco14 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 014 15 @deco16 def bulkitem_promotion(order):17     """18     单个商品为20个及以上提供10%的折扣19     """20     discount = 021     for item in order.cart:22         if item.quantity >= 20:23             discount += item.total()*0.124     return discount25 26 @deco27 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 034 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 = 42 def f(x):3     print(x)4     print(y)5 6 f(3)

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

1 y = 42 def f(x):3     print(x)4     print(y)5     y += 16     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 = 42 def f(x):3     print(x)4         global y5     print(y)6     y += 17     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))

 

 

转载于:https://www.cnblogs.com/forwardfeilds/p/10475306.html

你可能感兴趣的文章
【C++】const在不同位置修饰指针变量
查看>>
github新项目挂历模式
查看>>
编写jquery插件
查看>>
敏捷开发笔记
查看>>
神秘海域:顶级工作室“顽皮狗”成长史(下)
查看>>
C++指针、引用知多少?
查看>>
services 系统服务的启动、停止、卸载
查看>>
Fiddler 网页采集抓包利器__手机app抓包
查看>>
Number and String
查看>>
java中的值传递和引用传递2<原文:http://blog.csdn.net/niuniu20008/article/details/2953785>...
查看>>
css实现背景图片模糊
查看>>
什么是runtime?什么是webgl?
查看>>
秋季学习总结
查看>>
categorical_crossentropy VS. sparse_categorical_crossentropy
查看>>
强引用,弱引用,4种Java引用浅解(涉及jvm垃圾回收)
查看>>
多线程如何确定线程数
查看>>
UGUI RectTransform
查看>>
学前班
查看>>
手把手教您扩展虚拟内存
查看>>
android-samples-mvp
查看>>