Python装饰器:函数闭包的语法糖
预备知识
闭包:又称为闭包函数或者闭合函数。闭包是一种特殊的函数,这种函数由两个函数的嵌套组成,且称之为外函数和内函数,外函数返回值是内函数的引用,此时就构成了闭包。格式如下:
def 外层函数(参数):
def 内层函数():
print("内层函数执行", 参数)
return 内层函数
内层函数的引用 = 外层函数("传入参数")
内层函数的引用()
1.什么是装饰器
??装饰器就是一个闭包,装饰器是闭包的一种应用。用于拓展原来函数功能的一种函数,可以在不更改原函数代码的前提下给函数增加新的功能。
2.为什么要使用装饰器
装饰器的使用符合了面向对象编程的开放封闭原则。 ? 开放封闭原则主要体现在两个方面:
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。
业务用途:装饰器经常用于有切面需求的场景。比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。
3.引出装饰器
需求:为下列info函数添加统计运行时间的辅助功能。
def info():
print('这是一个函数!')
info()
解决方案一:在函数源代码上添加运行时间功能。
import time
def info():
start = time.time()
print('这是一个函数!')
stop = time.time()
print(stop - start)
info()
缺点:函数逻辑和辅助功能耦合在一起了。不方便修改,容易引起bug。
解决方案二:将辅助功能从函数中抽离。定义辅助函数count_time,在辅助函数count_time调用函数info
import time
def count_time(func):
start = time.time()
func()
stop = time.time()
print(stop - start)
def info():
print('这是一个函数!')
count_time(info)
优点:解耦,函数职责分离。 缺点:要通过辅助函数来调用主要功能函数,不方便。
解决方案三:在调用主要功能函数时自动完成对运行时间的统计。通过闭包增强主要功能函数count_time,给它增加一个运行时间统计功能。
import time
def count_time(func):
def improved_func():
start = time.time()
func()
stop = time.time()
print(stop - start)
return improved_func
def info():
print('这是一个函数!')
info = count_time(info)
info()
缺点:需要显式进行闭包增强。即info = count_time(info)
4.语法糖:让你开心的语法
??语法糖指计算机中添加的某种语法,这种语法对语言的功能没有影响,但是更方便使用。 ??对上述解决方案三使用@操作,可以省去显式闭包增强的步骤,直接调用info() 即可得到想要的结果。@count_time等价于info = count_time(info)
import time
def count_time(func):
def improved_func():
start = time.time()
func()
stop = time.time()
print(stop - start)
return improved_func
@count_time
def info():
print('这是一个函数!')
info()
优点:利用装饰器及其语法糖操作,提高了程序的可重复利用性,并增加了程序的可读性。
5.装饰器进一步理解
思考:为什么装饰器要像解决方案三一样,必须写内嵌函数,像下面这样不行吗?
import time
def count_time(func):
start = time.time()
func()
stop = time.time()
print(stop - start)
@count_time
def info():
print('这是一个函数!')
info()
执行如下代码,观察结果: 执行一
import time
def count_time(func):
start = time.time()
func()
stop = time.time()
print(stop - start)
return func
@count_time
def info():
print('这是一个函数!')
输出结果如下:
这是一个函数!
0.0
观察结果:
- @count_time等价于info=count_time(info)
- 执行一中的装饰器没有嵌套内函数
- 没有对info进行调用也输出了装饰效果
执行二
import time
def count_time(func):
start = time.time()
func()
stop = time.time()
print(stop - start)
return func
@count_time
def info():
print('这是一个函数!')
info()
info()
输出结果如下:
这是一个函数!
0.0
这是一个函数!
这是一个函数!
观察结果:
- 当两次调用info()的时候,装饰器函数只被调用了一次
- 执行二中的装饰器也没有嵌套内函数
结果分析:
- 在执行一中,info利用@count_time进行装饰,运行即执行info=count_time(info)这句代码,在count_time中,info作为参数传入,先调用函数info(),返回info的指针,赋值给info。count_time(info)的返回值其实就是函数info()的地址。
- 在执行二中,由于@count_time等价于info= count_time(info)这句代码,即@count_time可以改写成info= count_time(info),只执行了一次,因此装饰器函数只被调用了一次。后续两次调用info(),其实与info()是否被装饰无关。
- 要确保每次调用功能函数时,装饰器都能被调用,必须使用内嵌包装函数,即装饰器必须为闭包函数。内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象。
6.多个装饰器的原理
观察如下执行结果
def dec1(func):
print("1111")
def one():
print("2222")
func()
print("3333")
return one
def dec2(func):
print("aaaa")
def two():
print("bbbb")
func()
print("cccc")
return two
@dec1
@dec2
def test():
print("test test")
test()
aaaa
1111
2222
bbbb
test test
cccc
3333
Tips:装饰器的外函数和内函数之间的语句是没有装饰到目标函数上的,而是在装载装饰器时的附加操作。即上面的print("1111") 和print("aaaa")
执行顺序分析 ??可以看到,多个装饰器的情况下,先是自底向上完成装饰器的定义,然后自顶向下执行装饰器的功能。17~20行是自底向上完成装饰器的过程,顺序是test=dect1(dect2(test)),即test被赋值为dect1内函数,作为指针指向内函数的内存地址;22行是自顶向下执行被装饰函数的过程,调用了test指针指向的函数,即test()。 自底向上完成装饰器 step1:先执行dect2(test),dect2对test进行装饰。输出aaaa,返回函数two(函数two中的func指向函数test)。 step2:然后执行dect1(two),dect1对two进行装饰。输出1111,返回函数one(函数one中的func指向函数two)。 step3:test被赋值,作为指针指向one函数。 自顶向下执行装饰器 step4:调用test(),即执行one函数,one() step5:在函数dect1的内函数one中,输出2222,func指向函数two,跳到函数two,执行函数two step6:在函数dect2的内函数two中,输出bbbb,func指向函数test,跳到函数test,执行函数test step7:函数test中,结果是输出test test,函数test执行完毕 step8:回到函数dect2的内函数two中,输出cccc,函数two执行完毕 step9:回到dect1的内函数one中,输出3333,函数one执行完毕 step10:整个装饰执行过程完毕!
7.被装饰函数带参数和返回值
问题:上述被装饰函数均没有参数和返回值,对于带参数和返回值的函数,装饰器又该是什么样的呢?
探索一:带参数
例如,对如下函数添加计算运行时间的装饰器。
def add(x=1,y=2):
return x+y
直接运用上述count_time装饰器进行探寻。
import time
def count_time(func):
def improved_func():
start = time.time()
func()
stop = time.time()
print(stop - start)
return improved_func
@count_time
def add(x,y):
print('x+y')
add(1,2)
运行结果如下: add(1,2) TypeError: improved_func() takes 0 positional arguments but 2 were given
装饰器count_time无法传入函数add需要的参数值,因此需要接收函数add的参数。改写如下:
import time
def count_time(func):
def improved_func(*args,**kwargs):
start = time.time()
func(*args,**kwargs)
stop = time.time()
print(stop - start)
return improved_func
@count_time
def add(x,y):
print(x+y)
add(1,2)
运行结果如下:
3
0.0
探索二:带参数和返回值 例如,对如下函数添加计算运行时间的装饰器。
def add(x,y):
return x+y
直接运用探索一接收参数的count_time装饰器进行探索。
import time
def count_time(func):
def improved_func(*args,**kwargs):
start = time.time()
func(*args,**kwargs)
stop = time.time()
print(stop - start)
return improved_func
@count_time
def add(x,y):
return x+y
print(add(3,4))
运行结果如下:
0.0
None
装饰器count_time无法返回函数add的返回值,因此需要返回函数add的返回值。改写如下:
import time
def count_time(func):
def improved_func(*args,**kwargs):
start = time.time()
ret = func(*args,**kwargs)
stop = time.time()
print(stop - start)
return ret
return improved_func
@count_time
def add(x,y):
return x+y
print(add(3,4))
运行结果如下:
7
0.0
8.装饰器自带参数
需求:当多个函数需要统计运行时间时,为了输出显示是哪个函数的运行时间,装饰器自身需要带参,传入参数为被装饰函数的函数名称,因此可在上述装饰器的外层再嵌套一个函数,参数为装饰器自身参数。如下所示:
import time
def dec(func_name):
def count_time(func):
def improved_func(*args,**kwargs):
start = time.time()
ret = func(*args,**kwargs)
stop = time.time()
print('{}运行时间为'.format(func_name),stop - start)
return ret
return improved_func
return count_time
@dec('add函数')
def add(x,y):
return x+y
print(add(1,2))
运行结果为:
add函数运行时间为 0.0
3
|