【摘要】上篇博文介绍了闭包,闭包还有一个常用场景就是装饰器。因此,本篇博文将介绍装饰器
1.装饰器
试想一下这个场景,如果我们现在有一份代码,可以给用户提供打游戏、看视频,但是产品经理突然告知我们要对用户进行年龄判断,如果是未成年人,则这两个程序不对用户执行;如果是成年人,则执行相应程序。那么,难道我们要改代码吗?
又或者,我们需要在每一次的节日都打印该节日快乐,难道要频繁更改代码内容吗?
在Python中,针对这一情况,我们可以使用装饰器来予以解决。
1.1 装饰器的概念
装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景。
1.2 为何使用装饰器
有一些场景,需要频繁更改逻辑,但是遵循 开放封闭 原则,虽然在这个原则是用于面向对象开发,但是也适用于函数式编程。简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展。即:
封闭:已实现的功能代码块
开放:对扩展开发
装饰器可以帮助我们:
1).快速的在不改变原有函数的基础上,添加逻辑信息
2).不改变函数的调用方式
来看一个简单的例子(无参数的函数),假如下面这两个就是已经实现好的播放视频以及游戏代码块
def videos():
print("正在播放视频.....")
def game():
print("正在进入游戏.....")
现在我们要对未成年人进行限制,并且不能在封装好的代码块中修改。所以我们只能使用装饰器,而装饰器其实就是闭包的一个应用场景,那肯定要满足闭包的三个条件;
age = 13
#1). 外部函数里面也定义了一个函数
def is_adult(fun):
def wrapper():
if age >= 18:
2).内部函数调用了外部函数的临时变量
fun()
else:
print("未成年")
# 3).返回的是函数的引用
return wrapper
上面的代码就是我们所说的装饰器。那么如何使用装饰器呢?
#用装饰器is_adult装饰函数videos,并将返回只wrapper赋值给videos函数
videos=is_adult(videos)
#调用videos函数——>实质上就是调用wrapper函数内部代码
videos()
由于上面age变量的值为13,因此在调用wrapper函数后,执行了对age的判断,<18打印了未成年
如果把age变量赋值为35
age = 35
再调用上面的代码
videos=is_adult(videos)
videos()
即:
则执行效果如下:
但是这样装饰函数,实属麻烦。python中的语法糖可以给我们提供同样的作用,帮我们装饰需要装饰的函数,其语法如下:(只需要在代码块上面添加:@装饰器名称)
@is_adult #######语法糖所执行的效果就是:videos = is_adult(videos)
def videos():
print("正在播放视频.....")
@is_adult #######语法糖所执行的效果就是:game = is_adult(game)
def game():
print("正在进入游戏.....")
再来一个听音乐的代码块,没有用装饰器装饰
def music():
print("正在播放音乐.....")
我们现在给变量age赋值13,然后依次调用这三个函数,执行效果如下:
如果变量age=19,执行效果如下:
这是最简单的装饰器例子,无参数的函数装饰器。在下面的例子中,我们还需要掌握被装饰的函数有参数、被装饰的函数有可变参数和关键字参数、被多个装饰器装饰的函数,最后总结出通用装饰器的模板。
2.装饰器的应用场景
2.1 插入日志
日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述(比如:每个事件发生时的数据都是不同的)
模块logging的使用具体可参考官方文档link.
日志的函数如下:
日志的级别如下:
简单的例子介绍一下logging的使用:
import logging
#日志的基本配置
"""
对 basicConfig() 的调用应该在 debug() , info() 等的前面。因为它被设计为一次性的配置,只有第一次调用会进行操作,随后的调用不会产生有效操作。
"""
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',
level=logging.INFO,filename='example.log')
# 要更改用于显示消息的格式,你需要指定要使用的格式format,其中levelname (严重性), message (事件描述,包括可变数据),asctime(时间)
"""
如果多次运行上述脚本,则连续运行的消息将追加到文件 example.log 。
如果你希望每次运行重新开始,而不是记住先前运行的消息,则可以通过将上例中的调用更改为来指定 filemode 参数为w。因为filemode的默认值是a
"""
logging.debug('This message should appear on the console111')
logging.info('So should this111')
logging.warning('And this, too111')
由于之前已经打印过内容在example.log中,并且在basicConfig中没有指定filemode=‘w’,因此会在原本的example.log中追加info()和warning()内容。
下面讲一下插入日志的装饰器如何下:
首先我们若使用loggin模块,先对basicConfig做配置。
import logging
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',
level=logging.DEBUG,filename='example.log',
filemode='w',datefmt='%m/%d/%Y %I:%M:%S %p')
"""
日期/时间显示的默认格式类似于 ISO8601 或 RFC 3339 。
如果你需要更多地控制日期/时间的格式,请为 basicConfig 提供 datefmt 参数,
"""
#日志配置格式也可以这样写format = '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
然后写一个名为add_log的装饰器。
def add_log(func):
""""插入日志的装饰器"""
def wrapper(*args,**kwargs): #形参,args是元组,kwargs是字典
logging.debug('%s program starts' %(func.__name__))
result = func(*args,**kwargs) #实参,*args,**kwargs是解包
logging.debug('%s program stops' %(func.__name__))
return result
return wrapper
然后装饰在一个很简单无参数的login函数上。
@add_log
def login():
print('登陆成功')
我们调用login()函数,产生了example.log文件,其中记载了我们调用login函数所产生的日志信息。
但是这里要注意一个点,我们知道调用login函数,实质上是执行wrapper里面的内容,因此如果我们要打印出login的函数名,结果是wrapper。
print(login.__name__)
如果要保持被装饰函数func原有的函数名和帮助文档时,我们需要用到一个内置装饰器wraps。全部代码如下:
from functools import wraps
def add_log(func):
"""
插入日志的装饰器
"""
@wraps(func) #保持func原有的函数名和帮助文档,
#否则被装饰函数的函数名和帮助文档是wrapper
def wrapper(*args,**kwargs):
"""
闭包函数wrapper
"""
logging.debug('%s program starts' %(func.__name__))
result = func(*args,**kwargs)
logging.debug('%s program stops' %(func.__name__))
return result
return wrapper
@add_log
def login():
"""
login函数
"""
print('登陆成功')
login()
print(login.__name__)
print(login.__doc__)
打印结果如下:
下面介绍一个不使用logging模块的插入日志的装饰器:
首先分析,日志要显示的内容有:当前时间、主机名、当前正在运行的程序名称、运行结果
我们的思路是:利用time模块获取当前时间,利用os模块获取主机名,再用sys模块获取程序名称,最后对这些字段进行拼接。装饰器内容如下:
def add_log(fun):
def wrapper(*args, **kwargs):
# 获取被装饰的函数的返回值
result = fun(*args, **kwargs)
# 返回当前的字符串格式时间
now_time = time.ctime()
# 获取主机名 nodename='foundation0.ilt.example.com'
hostname = os.uname().nodename.split('.')[0]
# 获取运行的程序
process_full_name = sys.argv[0]
process_name = os.path.split(process_full_name)[-1]
# 日志内容
# 获取函数名: 函数名.__name__
info ="函数[%s]的运行结果为%s" %(fun.__name__, result)
log = " ".join([now_time, hostname, process_name, info])
print(log)
return result
return wrapper
def wrapper(*args, **kwargs)这一行代码中的 *args, **kwargs表示接收函数调用时的任意传参。其中 *args接收到的是一个元组, **kwargs接收到的是一个字典。
fun(*args, **kwargs)是将接收到的参数解包。
获取主机名的代码为什么那样写,可以看下图,就会比较明晰:
以及获取当前正在运行的程序名代码实现:
装饰器写好后,我们来装饰其他函数
在register函数上面使用语法糖,调用add_log这个装饰器来装饰register函数。所作的流程如注释。即在第42行调用register函数时,其实是将该函数中的参数传给装饰器中的wrapper函数,并执行。
定义wrapper函数时,参数 *args, **kwargs接收所有任意参数,接收成功后,调用fun(*args, **kwargs)执行register函数体,函数体是打印参数(name、age、province、gender)的值,并将返回值赋给result。
再执行warpper函数体下面的内容。
2.2 性能测试
下面我们写一个可计算程序运行速度的装饰器。
from functools import wraps
import time
def timeit(func): #step2:timeit(download_music)
"""计算程序运行时间的装饰器"""
@wraps(func) #保持被装饰函数的原有函数名和帮助文档
def wrapper(*args,**kwargs): #step5 :wrapper('yellow','D:/Music')
"""闭包函数wrapper"""
start_time = time.time()
result = func(*args,**kwargs) #step6:func=download_music -->download_music('yellow','D:/Music')
end_time = time.time()
print('%s运行时间为%.3f' %(func.__name__,end_time-start_time))
return result
return wrapper #setp3:download_music = wrapper
@timeit #step1:download_music = timeit(download_music) --> setp3:download_music = wrapper
def download_music(name,dir_path): #step7
"""下载音乐的函数"""
time.sleep(0.8)
print('%s已下载至%s' %(name,dir_path))
download_music('yellow','D:/Music') #step4
print('download_music的函数名:',download_music.__name__)
print('download_music的帮助文档:',download_music.__doc__)
结果如下:
2.3 事务处理
例如我们要写一个将所有数据转换成json格式的装饰器。
from functools import wraps
import string
import json
def json_dumps(func): #step2:json_dumps(get_userInfo) -->wrapper
@wraps(func)
def wrapper(*args,**kwargs): #step5
result = func(*args,**kwargs) #step6:result = get_userInfo
return json.dumps(result)
return wrapper
@json_dumps #step1:get_userInfo = json_dumps(get_userInfo) --> step3:get_userInfo=wrapper
def get_userInfo(): #step7
userInfo = {'user'+item:'passwd'+item for item in string.digits}
return userInfo
result = get_userInfo() #step4
print(type(result))
print(result)
结果如下:
2.4 缓存
functools.lru_cache的作用主要是用来做缓存,把相对耗时的函数结果进行保存,避免传入相同的 参数重复计算。同时,缓存并不会无限增长,不用的缓存会被释放。
"""fib的递归写法"""
def fib2(num):
if num in (1,2):
return 1
else:
return fib2(num-2)+fib2(num-1)
from functools import lru_cache
"""fib的递归写法但是加了cache"""
@lru_cache(maxsize=10000)
def fib3(num):
if num in (1,2):
return 1
else:
return fib3(num-2)+fib3(num-1)
@timeit
def use_cache():
result = fib3(40)
print(result)
@timeit
def no_cache():
result = fib2(40)
print(result)
use_cache()
no_cache()
执行结果:
可以明显看出使用了缓存后的fib数列加载的就很快。下面我们自己写一个fib数列的缓存装饰器。
def fib_cache(func):
"""自定义的fib数列缓存的装饰器"""
cache = {1:1,2:1,3:2,4:3}
def wrapper(num):
if num in cache: #若要调用的num在缓存中,则直接返回num对应的value值
return cache.get(num)
else: #若在缓存中没有对应的num,则执行func(即fib函数),将得到的value值存入cache并返回
result = func(num)
cache[num] = result
return result
return wrapper
@fib_cache
def fib4(num):
"""加了自定义fib_cache装饰器的函数"""
if num in (1,2):
return 1
else:
return fib4(num-2)+fib4(num-1)
def fib2(num):
"""没有缓存的fib函数"""
if num in (1,2):
return 1
else:
return fib2(num-2)+fib2(num-1)
@timeit
def use_cache():
result = fib4(40)
print(result)
@timeit
def no_cache():
result = fib2(40)
print(result)
use_cache()
no_cache()
执行结果如下:
3.通用装饰器的模板
掌握下面这个模板,装饰器就稳了~只需要在里面添加内容即可。
def decorate(fun):
def wrapper(*args, **kwargs): # args, kwargs是形参
# 在函数之前做的操作
result = fun(*args, **kwargs) # *args, **kwargs是实参, *args, **kwargs是在解包
# result接收被装饰函数的返回值;
# 在函数之后添加操作
return result
return wrapper
#@装饰器的名字
@decorate ===> add=decorate(add) ---> add指向wrapper函数位置
def add():
return 'ok'
#调用被装饰的函数
add()
4.多个装饰器装饰
上面我们看了一个装饰器装饰一个函数的例子,如果是多个装饰器装饰一个函数,执行流程如何呢?
例子一
这里有两个装饰器,分别为:1). 判断用户是否登录?2). 判断用户是否有权限?
def is_login(fun):
def wrapper1(*args, **kwargs):
print("判断是否登录......")
result = fun(*args, **kwargs)
return result
return wrapper1
def is_permission(fun):
def wrapper2(*args, **kwargs):
print("判断是否有权限......")
result = fun(*args, **kwargs)
return result
return wrapper2
用这两个装饰器装饰函数delete()
@is_login
@is_permission
def delete():
print('正在删除学生信息')
return '删除学生信息完毕'
执行被装饰的delete(),用result接收delete()的返回值,并打印。
result = delete()
print(result)
执行效果如下:
用这个简单的例子,来分析一下执行流程:
1). delete = is_perssion(delete) # delete实际上是wrapper2
先用下面这个装饰器is_permission装饰delete函数,装饰完之后,delete函数指向wrapper2。
2). delete = is_login(delete) # delete = is_login(wrapper2) # delete实际上是wrapper1
再用上面的装饰器is_login装饰delete函数(此时的delete函数其实是wrapper2),装饰完wrapper2函数后返回给delete,delete函数此时指向wrapper1
上面这两步是装饰过程,装饰完成之后,再调用detele函数
3). delete() —> wrapper1() —> wrapper2() —> delete()
此时调用delete函数其实就相当于调用wrapper1,进入wrapper1函数体内执行print(“判断是否登录…”),再执行 result = fun(*args, **kwargs),但其实这个fun指的是wrapper2函数,因此这句调用执行wrapper2函数体。
先执行print(“判断是否有权限…”),再执行 result = fun(*args, **kwargs)。这个fun才是真正的delete函数,执行delete函数体内容,并将delete函数的返回值获取到赋值给wrapper2中的 result,wrapper2函数体执行完毕后将返回值result带回给wrapper1函数体中的result,执行完wrapper1中的函数体将返回值result带回给调用语句result = delete()的result,最后打印这个返回值。
嗯,是有点绕,如果分析不出来其执行流程的话,记住以下两句话:
1.装饰过程: 函数先被下面的装饰器装饰,后被上面的装饰器装饰
2.执行过程:上面装饰器先执行, 下面的装饰器后执行
例子二
假设我们要执行删除学生信息的程序,但是要求1)用户登陆成功 2)用户拥有权限
下面看一下代码:
首先系统用户的信息应该存储在数据库中,用户登陆时获取到用户登陆信息,与数据库中的信息作匹配,这里我们用字典存储系统中所有用户的信息。只有两个用户root和admin。
db = {
'root': {
'name': 'root',
'passwd': '123',
'is_super': 1 # 0-不是 1-是
},
'admin': {
'name': 'admin',
'passwd': '456',
'is_super': 0 # 0-不是 1-是
}
}
再用一个空字典,来存储当前登陆用户的信息
login_user_session = {}
下面是装饰器is_login,用来判断用户是否登录, 如果没有登录,先登录
def is_login(fun):
def wrapper1(*args, **kwargs):
if login_user_session:
result = fun(*args, **kwargs)
return result
else:
print("请您登陆".center(50, '*'))
user = input("User: ")
passwd = input('Password: ')
if user in db:
if db[user]['passwd'] == passwd:
login_user_session['username'] = user
print('登录成功')
# ***** 用户登录成功, 执行删除学生的操作;
result = fun(*args, **kwargs)
return result
else:
print("密码错误")
else:
print("用户不存在")
return wrapper1
第二个装饰器是is_permission,用来判断用户是否有权限进行删除操作。
def is_permission(fun):
def wrapper2(*args, **kwargs):
print("判断是否有权限......")
current_user = login_user_session.get('username')
permissson = db[current_user]['is_super']
if permissson == 1:
result = fun(*args, **kwargs)
return result
else:
print("用户%s没有权限" % (current_user))
return wrapper2
用这两个装饰器来装饰函数delete。
@is_login
@is_permission
def delete():
return "正在删除学生信息"
装饰过程是:
1). delete = is_permission(delete) # delete = wrapper2
先用装饰器 is_permission装饰delete函数,装饰完后,delete指向wrapper2
2). delete = is_login(delete) # delete = is_login(wrapper2) # delete = wrapper1
再用装饰器 is_login装饰delete函数(实质上是wrapper2),装饰完后,delete指向wrapper1
调用函数:
result = delete()
print(result)
被调用的过程是:
delete( ) ------> wrapper1( ) —> wrapper2( ) —> delete( )
执行delete( ),实际上是执行wrapper1( )的函数体,wrapper1中的fun参数是接收到的wrapper2函数,因此执行wrapper2;warpper2( )函数体中的fun参数是delete函数,执行完后,再一步一步将返回值带回。最后打印。
执行效果如下:
5.带参数的装饰器
如果装饰器需要传递参数, 在原有的装饰器外面嵌套一个函数即可。
直接看例子,假如我们这里对用户登陆有限制,只能本地用户登陆,不支持远程用户登录,可以通过装饰器接收参数,来分别执行。
以下为装饰器,注意要在外部嵌套一个函数
def auth(type):
def wrapper1(fun):
def wrapper(*args, **kwargs):
if type=='local':
user = input("User:")
passwd = input("Passwd:")
if user == 'root' and passwd == '123':
result = fun(*args, **kwargs)
return result
else:
print("用户名/密码错误")
else:
print("暂不支持远程用户登录")
return wrapper
return wrapper1
用这个装饰器auth来装饰home函数
@auth(type='remote')
def home():
print("这是主页")
这里要注意的是:上面我们语法糖的结构是@funName 。
这里的结构是@funName()。默认跟的不是函数名, 先执行该函数, 获取函数返回结果,再 @funNameNew
理解过程如下:
@后面跟的是装饰器函数的名称, 如果不是名称, 先执行,再和@结合。
1). @auth(type=‘local’)
2). wrapper1 = auth(type=‘local’)
3). @wrapper1
4). login = wrapper1(login)
4). login = wrapper2
一定要注意:
@函数名 add=函数名(add)
@函数名( ) @新的函数名
调用函数看效果:
home()
如果auth装饰器中的参数是local,执行结果为:
【官方装饰器案例】
https://wiki.python.org/moin/PythonDecoratorLibrary