python有很多灵活的语法,闭包和装饰器就是其中很常见很实用的设计。
闭包和装饰器整体思路采用“面向切面编程”的理念,用来实现一些辅助功能时非常好用,例如日志打印、时间计数、账户鉴权等。
本文将简单介绍原理,并结合几个简单案例说明。
内容大纲
1 闭包
1.1 概念
python的闭包,简而言之就是 :
在函数中再次嵌套一个函数,并且内部函数引用外部函数的变量,外部函数的返回值是内部函数
闭包作用就是将原函数做一些处理(如功能增强等)然后返回。
如下图实例所示:
定义一个函数aaa,再其内部定义另一函数bbb,并且函数aaa的返回值是函数bbb整体,此时便形成了一个闭包
aaa的作用是对原函数bbb进行处理(传入变量x),最后返回原函数bbb
def aaa(x):
def bbb(y):
return x - y
return bbb
print(aaa(6)(4))
1.2 闭包实例
闭包的常见用法就是将某些功能编写到闭包函数中,当有其他任何函数需要使用此功能时,直接显示调用一次即可。
举个例子,闭包就是裁缝店的老板提前制作出一些不同的帽子,当有客人需要的时候,他直接拿来戴上就行。不用客人自己自己制做
例:(此时的UseTime并不是最终的版本,因为其缺少参数传入,并且没有用wraps装饰)
def UseTime(func):
'''
定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
'''
def aaa():
import time
time_1 = time.time()
bbb = func()
time_2 = time.time()
total_time = time_2 - time_1
print("\n执行花费的时间为: {:.3} s".format(total_time))
return bbb
return aaa
def IsPrime(num):
'''判断某数字是不是质数'''
if num < 2:
return False
elif num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
else:
return True
def Total():
'''
统计1-10000之间有多少质数
'''
count = 0
for i in range(1, 10001):
if IsPrime(i):
count += 1
return count
if __name__ == '__main__':
# 将原函数Total通过闭包函数UseTime进行加工(增加时间统计功能)
Total = UseTime(Total)
print('质数共有 ' + str(Total()) + ' 个')
可以看到,上述代码主要有以下内容:
- 定义了一个闭包函数UseTime 来统计其他函数的执行时间,
- 定义了一个函数IsPrime 判断接收的数字是否是质数,
- 定义了一个函数Total 调用IsPrime 来判断1-10000中有多少个质数
- 最后的main函数中,让Total “戴上”能统计出其执行时间的 “帽子”
执行结果如下:
可以看到,打印的结果中,不仅包含了Total函数自身的统计结果,还包含了他的帽子——UseTime的执行结果
这种方法也被称为面向切面编程,即主函数只实现基本的功能,剩余的都由各种函数闭包来装饰,最终“拼接”实现一个完整的应用。
这种情况下,例如上述代码中的 Total = UseTime(Total) 这种显示声明看起来会很乱。
并且每次调用函数都会执行一次Total = UseTime(Total),相当于增强了很多次,这样就会出现执行上述代码,却打印了很多条执行时间的情况。
此时就需要使用 语法糖、装饰器来实现了
2 语法糖和装饰器
2.1 装饰器
装饰器本质上是闭包的语法糖,后续结合语法糖说明
装饰器被调用时有2点需要注意:
① 装饰器的增强时机是在第一次调用之前。
只有在第一次调用被装饰的函数时,闭包函数才会被调用,装饰器才会真正生效。
若整个程序的运行过程中都没有调用被装饰的函数,那闭包函数也不会被调用,意味着装饰器实际上是无效的。
② 装饰器只增强一次原函数
闭包函数只会被调用一次。
当第二次以上调用原函数时,实际上调用的直接就是增强后的函数(防止出现闭包函数“套娃”的情况)
2.1.1 使用方法
例如以上例子的代码 Total = UseTime(Total) ,其实可以在定义函数Total 时,使用@UseTime直接进行装饰
以上两种写法等价
2.1.2 参数 以及 @wraps
很多函数都需要传入参数,通过上述方式定义的装饰器无法接受参数,会报错。
所以还需要对其进一步优化:
from functools import wraps
def UseTime(func):
'''
定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
'''
def aaa(*args, **keyword):
import time
time_1 = time.time()
bbb = func(*args, **keyword)
time_2 = time.time()
total_time = time_2 - time_1
print("\n执行花费的时间为: {:.3} s".format(total_time))
return bbb
return aaa
其实在装饰器中,被装饰后的函数(内函数)已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响使用,Python的functools包中提供了一个叫wraps的装饰器来消除这样的副作用
所以无论写什么装饰器,最好都在实现之前加上functools的wrap,它能保留原有函数的名称和属性。
例如以下代码,在定义装饰器UseTime时,对其内函数使用wraps进行装饰:
from functools import wraps
def UseTime(func):
'''
定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
'''
@wraps(func)
def aaa(*args, **keyword):
import time
time_1 = time.time()
bbb = func(*args, **keyword)
time_2 = time.time()
total_time = time_2 - time_1
print("\n执行花费的时间为: {:.3} s".format(total_time))
return bbb
return aaa
以上代码是一个完整的常见装饰器
2.1.3 常用装饰器的写法
实际使用中,装饰器的写法都是较为固定的,基本都是以下的框架:
def UseTime(func):
'''
定义一个闭包函数,用于统计其他任何函数的执行时间,并打印提示信息
'''
@wraps(func)
def aaa(*args, **keyword):
# 此处可以新增加一些功能
bbb = func(*args, **keyword)
# 此处可以新增加一些功能
return bbb
return aaa
2.2 语法糖
装饰器之所以能用 @装饰器名 的方式来简写,根本原因是:装饰器本质上是闭包的语法糖
闭包之前举例说明过了,那什么是语法糖呢?可以吃吗...
语法糖指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。
语法糖其实只是一种代码的“简写方式” 它本身并没有增加任何新功能。
所以意味着包含语法糖的代码完全可以转化为不包含语法糖的写法,两者完全等价。
例如图中的2种写法是等价的,但右边的看起来更整洁
2.3 类装饰器
既然函数可以通过装饰器来增加新功能,那class是否也能用类似的方法来偷懒呢?
实际上是可以的,这也就是 类装饰器
具体的实现方法要比函数装饰器稍微复杂一些,但逻辑是差不多的。
以后用到的时候再补充吧,暂时偷个懒~