【摘要】上篇博文介绍了闭包,闭包还有一个常用场景就是装饰器。因此,本篇博文将介绍装饰器
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 插入日志
日志要显示的内容有:当前时间、主机名、当前正在运行的程序名称、运行结果
我们的思路是:利用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函数体下面的内容。
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
一定要注意:
@函数名 add=函数名(add)
@函数名( ) @新的函数名
调用函数看效果:
home()
如果auth装饰器中的参数是local,执行结果为:
【官方装饰器案例】
https://wiki.python.org/moin/PythonDecoratorLibrary