闭包和装饰器
1.8 闭包和装饰器
学习目标
1. 能够说出闭包的定义形式
2. 能够说出装饰器的实现形式
3. 能够说出装饰器的作用
4. 能够说出装饰器的不同形式
5. 能够说出万能装饰器的实现形式
6. 能够说出装饰器的执行过程
--------------------------------------------------------------------------------
1.8.1 闭包和装饰器概述
什么是闭包:
闭包是指在一个函数中定义了一个另外一个函数,内函数里运用了外函数的临时变量(实际参数也是临时变量),并且外函数的返回值是内函数的引用(一切皆引用,所有的函数名字都只是函数体在内存空间的一个引用。)
闭包的作用:
可以隐藏内部函数的工作细节,只给外部使用者提供一个可以执行的内部函数的引用。
避免了使用全局变量,保证了程序的封装性
保证了内函数的安全性,其他函数不能访问
什么是装饰器:
装饰器就是用于拓展已有函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,实际上就是利用闭包语法实现的。
装饰器的作用
在不用更改原函数的代码前提下给函数增加新的功能。
1.8.2 思考
有一种叫做五步棋的游戏,在一个 五行五列的网格上双方各持一色棋子,在棋子移动时,只能以横向或纵向的形式移动。
如果实现游戏,如何能记录当前棋子移动的位置(也就是坐标)呢?
1.8.3 技术点回顾
在使用函数时,函数可以传递参数,函数也可以返回数据。
在传递和返回数据时,一般是传递返回的固定数据和代码执行的结果。
在Pyhton中,函数也是一个对象,在函数操作中,函数对象也可以当成一个参数或一个返回值进行返回。
当程序内或程序外拿到参数的引用后就可以直接使用这个函数,(原理回想下深浅拷贝中的赋值)
def show():
print('show run')
show()
func = show
func()
程序执行结果:
show run
show run
1.8.4 闭包
闭包就是在一个外部函数中定义了一个内部函数,并且在内部函数中使用了外部函数的变量,并返回了内部函数的引用。
nonlocal 的使用
nonlocal 变量名 ——》声明变量为非本地变量
如果在闭包的内部函数中直接使用外部函数的变量时,不需要任何操作,直接使用就可以了。
但是如果要修改外部变量的值,需要将变量声明为 nonlocal,那么建议将 nonlocal 写在内部函数的第一行。
利用函数可以被传递和返回的特性,在开发过程中,可以隐藏更多的实现细节。
n = 1 # 全局变量
def show(): # 公有函数
print('show: ',n)
def callFunc(func): #公有函数
return func
s = callFunc(show) # 函数执行
s()
show()
在这段代码中,在实际开发中并没有实际意义,只是简单示意了函数可以被当做参数和返回值使用。
但是这段代码并不完美
第一,尽量不要使用全局变量,因为全局变量会破坏程序的封装性。
第二,如果 show 函数不想被 callFunc 以外的函数进行访问时,是无法控制的。
所以可以改进如下:
def callFunc():
n = 1
def show():
print('show: ', n)
return show
s = callFunc()
s()
# show() 因为 show 函数定义在 callFunc 内部,所以外部不可见,不能使用
代码改进后,去掉了全局变量的使用。而且将 show 函数封装在了 callFunc 函数内部,使外部不可见,不能使用 show 函数,隐藏了实现细节
程序在执行时,callFunc 函数返回了内部定义的 show 函数,并且 在 show 函数内部使用了外部函数的变量。
在 show 函数返回时,保存了当前的执行环境,也就是会在 show 函数中使用的外部变量 n 。
因为 n 是一个 callFunc 函数中的局部变量,正常情况下 callFunc 函数执行结束后,n 就会被释放。
但是现在因为 callFunc 函数中返回了 show 函数,show 函数在外部还会再执行,所以程序会将 show 函数所需的执行环境保存下来。
这种形式就是闭包。
利用闭包完成棋子的移动
'''闭包实现棋子移动'''
# 定义一个外部函数
def outer():
# 在外部函数中定义一个保存坐标的列表
position = [0,0]
# 定义一个内部函数,参数为移动方式和步长
# 移动方式为列表 [x,y] x,y分别只能取 -1,0,1三个值,表示反向,不动,正向
def inner(direction,step):
# 计算坐标值
position[0] = position[0] + direction[0] * step
position[1] = position[1] + direction[1] * step
# 返回移动后的坐标
return position
# 返回内部函数
return inner
# 获取内部函数
move = outer()
# 移动
print(move([1, 0], 10))
print(move([0, 1], 10))
print(move([-1, 0], 10))
程序执行结果:
[10, 0]
[10, 10]
[0, 10]
nonlocal 的使用 如果在闭包的内部函数中直接使用外部函数的变量时,不需要任何操作,直接使用就可以了。
但是如果要修改外部变量的值,需要将变量声明为 nonlocal
def callFunc():
m = 1
n = 2
def show():
print('show - m: ', m)
nonlocal n #如果不加会报错。
n *= 10
print('show - n: ', n)
return show
s = callFunc()
s()
nonlocal 声明变量为非本地变量,如果确定在程序要修改外部变量,那么建议将 nonlocal 写在内部函数的第一行。
小结: 闭包就是在一个外部函数中定义了一个内部函数,并且在内部函数中使用了外部函数的变量,并返回了内部函数。
1.8.5 装饰器
装饰器的定义:
不改变原有函数功能的基础上,对函数进行扩展的形式,称为装饰器。
装饰器的本质:
实际上就是一个以闭包的形式定义的函数 。
装饰器的作用:
为现有存在的函数,在不改变函数的基础上去增加一些功能进行装饰。
装饰器函数的使用:
在被装饰的函数的前一行,使用 @xxx (@装饰器(闭包)函数名) 形式来装饰
装饰器的好处:
定义好了装饰器(闭包)函数后,只需要通过 @xxx (@装饰器(闭包)函数名)形式的装饰器语法,将 @xxx (@装饰器(闭包)函数名) 加到要装饰的函数前即可。
装饰器的原理:
在执行 @xxx 时 ,实际就是将 原函数传递到闭包函数中,然后原函数的引用指向闭包返回的装饰过的内部函数的引用。
@count_time # 这实际就相当于解决方法3中的 my_count = count_tiem(my_count)(把被装饰的函数引用当作参数传入到装饰器函数,被装饰的函数引用指向装饰器函数返回的引用)
实例应用
现在一个项目中,有很多函数 ,由于项目越来越大,功能越来越多,导致程序越来越慢。
其中一个功能函数功能,实现一百万次的累加。
def my_count():
s = 0
for i in range(1000001):
s += i
print('sum : ', s)
现在想计算一下函数的运行时间,如何解决?如何能应用到所有函数上?
解决办法 1
start = time.time()
my_count()
end = time.time()
print('共计执行:%s 秒'%(end - start)) # 使用%d显示,取整后是0秒,因为不到一秒
这种办法是最简单的实现方式,但是一个函数没问题,但是要有1000个函数,那么每个函数都要写一遍,非常麻烦并且代码量凭空多了三千行。
这明显是不符合开发的原则的,代码太冗余
解决办法 2
def count_time(func):
start = time.time()
func()
end = time.time()
print('共计执行:%s 秒'%(end - start)) # 使用%d显示,取整后是0秒,因为不到一秒
count_time(my_count)
经过修改后,定了一个函数来实现计算时间的功能,通过传参,将需要计算的函数传递进去,进行计算。
修改后的代码,比之前好很多。
但是在使用时,还是需要将函数传入到时间计算函数中。
能不能实现在使用时,不影响函数原来的使用方式,而又能实现计算功能呢?
解决办法 3
def count_time(func):
def wrapper(): #wrapper 装饰
start = time.time()
func()
end = time.time()
print('共计执行:%s 秒'%(end - start)) # 使用%d显示,取整后是0秒,因为不到一秒
return wrapper
my_count = count_time(my_count)
my_count()
此次在解释办法2的基础上,又将功能外添加了一层函数定义,实现了以闭包的形式来进行定义
在使用时,让 my_count 函数重新指向了 count_time 函数返回后的函数引用。这样在使用 my_count 函数时,就和原来使用方式一样了。
这种形式实际上就是塌装饰器的实现原理。
之前我们用过装饰器,如:@property 等
那么是否可以像系统装饰器一样改进呢?
解决办法 4
import time
def count_time(func):
def wrapper(): #wrapper 装饰
start = time.time()
func()
end = time.time()
print('共计执行:%s 秒'%(end - start)) # 使用%d显示,取整后是0秒,因为不到一秒
return wrapper
@count_time # 这实际就相当于解决方法3中的 my_count = count_tiem(my_count)
def my_count():
s = 0
for i in range(10000001):
s += i
print('sum : ', s)
my_count()
这样实现的好处是,定义好了闭包函数后。只需要通过 @xxx 形式的装饰器语法,将 @xxx 加到要装饰的函数前即可。
使用者在使用时,根本不需要知道被装饰了。只需要知道原来的函数功能是什么即可。
这种不改变原有函数功能基础上,对函数进行扩展的形式,称为装饰器。
在执行 @xxx 时 ,实际就是将 原函数传递到闭包中,然后原函数的引用指向闭包返回的装饰过的内部函数的引用。
1.8.6 装饰器的几种形式
根据被装饰函数定义的参数和返回值定义形式不同,装饰器也对应几种变形。
无参无返回值
def setFunc(func):
def wrapper():
print('Start')
func()
print('End')
return wrapper
@setFunc
def show():
print('show')
show()
无参有返回值
def setFunc(func):
def wrapper():
print('Start')
return func()
return wrapper
@setFunc # show = setFunc(show)
def show():
return 100
print(show() * 100)
有参无返回值
def setFunc(func):
def wrapper(s):
print('Start')
func(s)
print('End')
return wrapper
@setFunc
def show(s):
print('Hello %s' % s)
show('Tom')
有参有返回值
def setFunc(func):
def wrapper(x, y):
print('Start')
return func(x, y)
return wrapper
@setFunc
def myAdd(x, y):
return x + y
print(myAdd(1, 2))
1.8.7 万能装饰器
万能装饰器的定义:
通过可变参数和关键字参数来接收不同的参数类型定义出来的装饰器函数适用于任何形式的函数。
def setFunc(func):
def wrapper(*args, **kwargs): # 接收不同的参数
print('wrapper context')
return func(*args, *kwargs) # 再原样传回给被装饰的函数
return wrapper
@setFunc
def show(name, age):
print(name,age)
show('tom',12)
1.8.8 类实现装饰形式
通过类的定义实现装饰器形式:
在类中通过使用 __init__ 和 __call__方法来实现
通过重写__init__初始化方法,接收参数,将要被装饰的函数传进来并记录下来(相当于外函数接收参数)
通过重写 __call__ 方法来实现装饰内容(相当于内函数实现装饰内容)
@Test ——》 show = Test(show) show由原来引用函数,装饰后变成 引用Test装饰类的对象
show() ——》实际上是仿函数(是在实现__call__魔法方法后,将对象当做函数一样去使用),即对象调用方法实现了装饰器
class Test(object):
# 通过初始化方法,将要被装饰的函数传进来并记录下来
def __init__(self, func):
self.__func = func
# 重写 __call__ 方法来实现装饰内容
def __call__(self, *args, **kwargs):
print('wrapper context')
self.__func(*args, **kwargs)
# 实际通过类的魔法方法call来实现
@Test # --> show = Test(show) show由原来引用函数,装饰后变成引用Test装饰类的对象
def show():
pass
show() # 对象调用方法,实际上是调用魔法方法call,实现了装饰器
1.8.9 函数被多个装饰器所装饰(了解)
一个函数在使用时,通过一个装饰器来扩展,可能并不能完成达到预期。
Python 中允许一个装饰器装饰多个函数和一个函数被多个装饰器所装饰。
多个装饰器的装饰过程:
从下向上装饰(即从里往外执行),先装饰函数,然后再向外(向上)一层一层装饰。
# 装饰器1
def setFunc1(func):
def wrapper1(*args, **kwargs):
print('Wrapper Context 1 Start...')
func(args, kwargs)
print('Wrapper Context 1 End...')
return wrapper
# 装饰器2
def setFunc2(func):
def wrapper2(*args, **kwargs):
print('Wrapper Context 2 Start...')
func(args, kwargs)
print('Wrapper Context 2 End...')
return wrapper
#一个函数被装饰了两次
@setFunc1
@setFunc2
def show(*args, **kwargs):
print('Show Run ...')
show()
程序执行结果 :
Wrapper Context 1 Start...
Wrapper Context 2 Start...
Show Run ...
Wrapper Context 2 End...
Wrapper Context 1 End...
这个装饰器的装饰过程是
从下向上装饰,即从里往外执行,先装饰函数,然后再一层一层装饰。
@setFunc2 -> show = setFunc2(show) -> show = setFunc2.wrapper2 @setFunc1 -> show = setFunc1(setFunc2.wrapper2) -> show = setFunc1.wrapper1(setFunc2.wrapper2(show))
1.8.10 装饰器传参:
装饰器在使用过程中,可能需要对装饰器进行传参
在定义可以传参的装饰器闭包时,需要定义三层函数
最外层函数用来接收装饰器的参数
中间层用来实现装饰器
最内层用来执行具体的装饰内容
无论有几层或者几个装饰器去装饰已有函数,最终函数都是引用装饰器的最内层的函数。
@xxx(xxx) 先执行传参 xxx(xxx) ,实际就是执行函数调用,得到中间层函数, 与@组合后变成装饰器形式,再进行装饰
# 定义一个路由字典
router = {}
# 实现一个装饰器,让这个装饰器来实现自动将 url 和 功能函数的匹配关系存到路由字典中
# 接收url参数
def set_args(args):
# 真正用来去装饰接收的函数
def set_func(func):
# 真正装饰函数
def wrapper(*args, **kwargs):
func()
# 因为 wrapper 就是指向被装饰的函数
router[args] = wrapper
return wrapper
return set_func
# 功能函数
# @set_args('login.html') -> @set_func
@set_args('login.html')
def login():
print('Login Run ...')
@set_args('nba.html')
def nba():
print('NBA Run ...')
@set_args('news.html')
def news():
print('News Run ...')
@set_args('11.html')
def double_one():
print('双十一')
# 模拟的运行函数
def run(url):
# 通过传入参数,也就是访问地址,来到路由字典中去找到对应的功能函数
func = router[url]
# 执行相应的功能函数
func()
# 模拟请求
run('login.html')
run('nba.html')
run('news.html')
run('11.html')
print(router)
1.8.11 总结:
1. 函数可以像普通变量一样,做为函数的参数或返回值进行传递
2. 函数内部可以定义另外一个函数,这样做的目的可以隐藏函数功能的实现
3. 闭包实际也是一种函数定义形式。
4. 闭包定义规则是在外部函数中定义一个内部函数,内部函数使用外部函数的变量,并返回内部函数的引用
5. Python 中装饰器就是由闭包来实现的
6. 装饰器的作用是在不改变现有函数基础上,为函数增加功能。
7. 通过在已有函数前,通过 @闭包函数名 的形式来给已有函数添加装饰器
8. 装饰器函数根据参数和返回值的不同,可细分为四种定义形式
9. 可以通过可变参数和关键字参数来实现能用装饰器定义形式
10. 一个装饰器可以为多个函数提供装饰功能,只需要在被装饰的函数前加 @xxx 即可
11. 通过类也可以实现装饰器效果,需要重写 __init__ 和 __call__ 函数
12. 类实现的装饰器在装饰函数后,原来的函数引用不在是函数,而是装饰类的对象
13. 一个函数也可以被多个装饰器所装饰,但是实际在使用时,并不多见,了解形式即可