day 13 闭包和装饰器
今日内容概要
- 闭包
- 装饰器初识
- 标准版装饰器
昨日内容回顾
-
推导式
-
列表推导式:
[i for i in range(3)] [i for i in range(9) if i % 3 == 0]
-
字典推导式:
{i: i + 1 for i in range(3)} {i: i + 1 for i in range(9) if i % 3 == 0}
-
集合推导式:
{i for i in range(3)} {i for i in range(9) if i % 3 == 0}
-
-
内置函数一
all() any() callable() hash() oct() hex() divmod() dir() help() complex() id() print() input() repr() frozenset() eval() # 禁用 exec() # 禁用 chr() ord() round() bytes()
-
内置函数二
# 高阶函数 abs() sum() max() min() map(规则函数,可迭代对象,可迭代对象) filter(规则函数,可迭代对象) zip() reversed(可迭代对象) sorted(可迭代对象,key=规则函数) reduce(累加函数,可迭代对象) format() enumerate pow() lambda 匿名函数 lambda 形参:返回值 lst = [1,2,3,4,5,7] print(list(reversed(lst))) print(lst)
今日内容详细
闭包
在编程时,我们会处理到很多数据。但是对于一些数据,我们只想使用,不想修改。我们可以使用闭包
来防止不经意间的数据修改。
闭包的作用主要有两个:
- 保护数据安全
- 保护数据干净性
满足下面两个条件的函数就是一个实用的闭包:
- 在嵌套函数内,使用非全局变量(且不使用本层变量)
- 将嵌套函数本身返回
我们可以用.__closure__
方法来查看一个函数是否是一个闭包。当返回值为None时,说明该函数不是闭包,当返回值中有变量的内存地址时,说明该函数是一个闭包。
例如,下面的函数就是一个闭包:
def func():
a = 10 # 自由变量
def foo():
print(a)
return foo
f = func()
print(f.__closure__)
输出的结果为:(<cell at 0x000001F7D6277648: int object at 0x00000000669D8190>,)
方法中的closure
就是闭包
的意思。返回的cell
是一个单元格
对象,用来储存闭包中需要使用到的变量。
当函数执行完成后(也就是fun()
这个步骤),函数体会被销毁。但是闭包foo
被返回给了f
,还会被用到,所以照常存在。而闭包中需要使用的变量a
,此时已经不在函数func
中,因为func
本身已经不复存在。变量a
在函数销毁后,升级为自由变量
。
闭包是怎么做到保护数据的作用的呢?
比如我们现在有这样一个需求:使用一个函数,每天输入当天营业额,打印出这几天的平均营业额。
如果我们把营业额的数据储存在列表中,放到全局,可以这样实现功能:
lst = []
def ave_turnover(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
ave_turnover(12000)
ave_turnover(15000)
ave_turnover(14000)
ave_turnover(12000)
ave_turnover(25000)
ave_turnover(21000)
ave_turnover(9000)
输出的结果为:
12000.0
13500.0
13666.666666666666
13250.0
15600.0
16500.0
15428.57142857143
如果有一天,我们不小心在全局对lst
列表做了修改:
lst = []
def ave_turnover(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
ave_turnover(12000)
ave_turnover(15000)
lst[1] = 10
ave_turnover(14000)
ave_turnover(12000)
ave_turnover(25000)
ave_turnover(21000)
ave_turnover(9000)
输出的结果为:
12000.0
13500.0
8670.0
9502.5
12602.0
14001.666666666666
13287.142857142857
这就导致了数据的改变,使得第三天之后的数据全都不准确。
如果我们把重要的数据lst
封装到闭包中保护起来,就不会有这种烦恼了:
def ave_turnover():
lst = []
def inner(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
return inner
f = ave_turnover()
f(12000)
f(15000)
lst = [10]
f(14000)
f(12000)
f(25000)
f(21000)
f(9000)
输出的结果为:
12000.0
13500.0
13666.666666666666
13250.0
15600.0
16500.0
15428.57142857143
对于闭包,除了.__closure__
方法之外,还有可以用来查看自由变量的.__code__.co_freevars
方法,和可以用来查看局部变量的.__code__.co_varnames
方法:
def ave_turnover():
lst = []
def inner(turnover_today):
lst.append(turnover_today)
ave = sum(lst) / len(lst)
print(ave)
return inner
f = ave_turnover()
print(f.__closure__) # 判定是否是一个闭包
print(f.__code__.co_freevars) # 查看自由变量
print(f.__code__.co_varnames) # 查看局部变量
输出的结果为:
(<cell at 0x0000027A31A27648: list object at 0x0000027A31AD8C08>,)
('lst',)
('turnover_today', 'ave')
没有将嵌套的函数返回也可以是一个闭包,但是这个闭包没有办法被使用:
def func():
a = 10
def foo():
print(a)
print(foo.__closure__)
func()
输出的结果为:(<cell at 0x000001BE85EA7648: int object at 0x00000000669D8190>,)
闭包也可以通过外层函数加两个括号的方法来调用:
def func():
a = 10
def foo():
print(a)
return foo
func()()
外层函数的参数也是局部变量,所以内层函数使用外层参数的形式也可以构成闭包:
def wrapper(a, b):
a = 11
b = 12
def inner():
print(a)
print(b)
return inner
a = 11
b = 12
ret = wrapper(a, b)
print(ret.__closure__)
print(ret.__code__.co_freevars)
print(ret.__code__.co_varnames)
ret()
返回的结果为:
(<cell at 0x00000255942F7648: int object at 0x00000000669D81B0>, <cell at 0x00000255942F7678: int object at 0x00000000669D81D0>)
('a', 'b')
()
11
12
闭包的应用场景主要有两个:
- 装饰器
- 防止数据被误改动
装饰器
在编程中,有很多约定俗成的规则。开放封闭原则就是其中很重要的一个。
开放封闭原则体现在两个方面:
- 对扩展开放,支持增加新功能
- 对修改源代码封闭,对调用方式的改变封闭
装饰器就是为了体现编程的开放封闭原则而存在的。
装饰器,顾名思义,就是在原有基础上额外添加新功能的工具。
我们有下面一组函数:
import time
def index():
time.sleep(0.5) # 休眠0.5秒,阻塞函数
print('i am in index')
def func():
time.sleep(0.8)
print('i am in func')
def foo():
time.sleep(0.4)
print('i am in func')
index()
func()
foo()
现在有这样一个需求:想要知道每一个函数运行的时间。
为了不改变函数的源代码,我们可以尝试着在调用函数前后加入查看时间戳的代码,尝试着计算函数的运行时间:
import time
def index():
time.sleep(0.5)
print('i am in index')
def func():
time.sleep(0.8)
print('i am in func')
def foo():
time.sleep(0.4)
print('i am in func')
start_time = time.time
index()
print(time.time - start_time)
start_time = time.time
func()
print(time.time - start_time)
start_time = time.time
foo()
print(time.time - start_time)
输出的结果为:
i am in index
0.500666618347168
i am in func
0.800020694732666
i am in func
0.4008512496948242
我们好像得到我们想要的结果了。但是不要忘了,我们一开始提到的,我们编程时需要遵循开放封闭原则。我们的确没有改变函数的源代码,但是调用函数时,需要增加代码。我们改变了函数的调用方式。
另一方面,我们在调用函数的过程中,使用了大量的重复代码:start_time = time.time
和print(time.time - start_time)
为了减少重复代码,我们或许可以尝试着使用函数,比如这样:
import time
def index():
time.sleep(0.5)
print('i am in index')
def func():
time.sleep(0.8)
print('i am in func')
def foo():
time.sleep(0.4)
print('i am in func')
def run_time(f):
start_time = time.time()
f()
print(time.time() - start_time)
ff = index
index = run_time
index(ff)
ff = func
func = run_time
func(ff)
ff = foo
foo = run_time
foo(ff)
在调用函数时,我之所以把函数写得这么复杂,而不是简单地写成这样:
run_time(index)
run_time(func)
run_time(foo)
是为了避免改变函数的调用方式。
可即便绞尽脑汁,函数最终的调用方式还是发生了变化——原本函数的调用是不需要参数的,增加功能后需要增加参数ff
。
不过我们已经离正确的解决办法非常近了,只差一步,就可以解决我们的困难。
这就需要结合我们今天刚刚学到的内容:闭包。
如果我们把功能函数整合为闭包,就可以满足编程的开放封闭原则,并且不会增加太多的重复代码:
import time
def index():
time.sleep(0.5)
print('i am in index')
def func():
time.sleep(0.8)
print('i am in func')
def foo():
time.sleep(0.4)
print('i am in func')
def run_time(f):
def inner():
start_time = time.time()
f()
print(time.time() - start_time)
return inner
index = run_time(index)
index()
func = run_time(func)
func()
foo = run_time(foo)
foo()
上面的这种给函数增加功能的方法就构成了Python中的装饰器。
因为装饰器在Python编程中十分好用,Python还专门为类似index = run_time(index)
的赋值运算设定了一个语法糖:
import time
def run_time(f):
def inner():
start_time = time.time()
f()
print(time.time() - start_time)
return inner
@run_time # 等价于 index = run_time(index)
def index():
time.sleep(0.5)
print('i am in index')
index()
语法糖必须要放在被装饰函数的正上方。虽然有些空行,Python解释器也能识别,但是阅读起来会很别扭:
@run_time
def index():
time.sleep(0.5)
print('i am in index')
标准装饰器
如果原函数中有参数,我们可以在装饰器的内部函数中设置加入参数:
def plugin(f):
def inner(user, pwd, hero):
print('外挂开启')
f(user, pwd, hero)
print('外挂结束')
return inner
@plugin
def gamming(user, pwd, hero):
print('打开游戏')
print(f'用户名:{user},密码:{pwd}')
print(f'选择英雄:{hero}')
print('游戏中')
print('游戏结束')
gamming('meet', '1234', '草丛伦')
输出的结果为:
外挂开启
打开游戏
用户名:meet,密码:1234
选择英雄:草丛伦
游戏中
游戏结束
外挂结束
其实,在装饰器中,我们可以使用*args
接收全部位置参数,使用**kwargs
接收全部关键字参数。在调用函数时,只需要把args
和**kwargs
重新打散即可:
def plugin(f):
def inner(*args, **kwargs):
print('外挂开启')
f(*args, **kwargs)
print('外挂结束')
return inner
@plugin
def gamming(user, pwd, hero):
print('打开游戏')
print(f'用户名:{user},密码:{pwd}')
print(f'选择英雄:{hero}')
print('游戏中')
print('游戏结束')
gamming('meet', '1234', '草丛伦')
修饰过的gamming
函数,不论是调用方法,还是参数使用,就是传参数后报错的内容都是跟修饰前的gamming
一致。这就完美地符合了编程的开放封闭原则。
除了使用参数,函数的另外一个特点是可以有返回值。如果被修饰的参数也有返回值,我们只需在装饰器的内层函数中加入返回值即可:
def plugin(f):
def inner(*args, **kwargs):
print('外挂开启')
a = f(*args, **kwargs)
print('外挂结束')
return a
return inner
@plugin
def gamming(user, pwd, hero):
print('打开游戏')
print(f'用户名:{user},密码:{pwd}')
print(f'选择英雄:{hero}')
print('游戏中')
print('游戏结束')
return '我卢**没有开挂!'
print(gamming('meet', '1234', '草丛伦'))
输出的结果为:
外挂开启
打开游戏
用户名:meet,密码:1234
选择英雄:草丛伦
游戏中
游戏结束
外挂结束
我卢**没有开挂!
至此,我们已经把装饰器的内容都讨论过了。其实,装饰器本身并不复杂,我们可以使用一个很规整简洁的代码写出一个装饰器:
def wrapper(func):
def inner(*args, **kwargs):
"""执行被装饰函数前,进行的操作"""
ret = func(*args, **kwargs)
"""执行被修饰函数后,进行的操作"""
return ret
return inner
@wrapper
def foo():
print('in foo')
foo()
这就是标准版的装饰器
。
其实,对于没有参数的函数,可以只写一层函数实现装饰器的功能:
def foo(func):
print('新加了一个功能')
return func
@func
def index():
print(2)
index()
输出的结果为:
新加了一个功能
2
虽然也能实现新功能,但是不建议这样写,因为不是很规范。而且下面继续调用index,并不会有新功能加入:
def foo(func):
print('新加了一个功能')
return func
@func # index = func(index) --> index = index
def index():
print(2)
index()
index()
输出的结果为:
新加了一个功能
2
2
函数调用两次,但是新加功能仅执行了一次。这是因为在执行语法糖时,新的index
最终的结果还是原来的index
函数,而不是像标准装饰器中的闭包
。