装饰器
一、装饰器基础
1、什么是装饰器?
装饰器就是一个返回函数的高阶函数。顾名思义,装饰器就是对现有的函数的装饰,在不改变现有函数的的源代码以及调用方式的基础上添加新的功能。所以,一个装饰器应该满足以下三点原则:
- 不改变现有函数的源代码(开放封闭原则)
- 不改变现有函数的调用方式
- 能够添加满足需求的新功能
2、实现基础
如何实现装饰器的三条原则呢?需要使用以下三个知识点:
- 高阶函数
- 函数嵌套
- 闭包
3、高阶函数
高阶函数就是变量可以指向函数,并且函数的参数能接收变量,那么一个函数就可以接受另一个函数作为参数,最后返回值可以是函数。所以一个高阶函数满足以下两点之一就是高阶函数。
- 函数接收的参数是一个函数名
- .函数的返回值是一个函数名
如果仅仅通过高阶函数能否满足装饰器的原则呢?假如现在有一个需求,计算一个函数的的运行时间,一般情况下应该这样就可以实现的。
import time def Bar(): start_time=time.time() time.sleep(2) print("我是bar函数") end_time=time.time() return end_time-start_time res=Bar() print(res) ###################输出结果########## #我是bar函数 #2.0001144409179688
这样是可以实现的,但是如果需要为1000或者更多函数添加此项功能呢?在不违背开放封闭的前提下,只能在原有功能的基础上进行装饰Bar函数。
#函数接收的参数是一个函数名 import time def Bar(): time.sleep(2) print("我是bar函数") def CalTime(func): start_time=time.time() func() end_time=time.time() return end_time-start_time res=CalTime(Bar) print(res) ######################输出############# #我是bar函数 #2.0001144409179688
这里应用高阶函数的第一点,传入函数名,可以看到CalTime函数实现了计算时间功能,并且没有改变Bar函数源代码,但是函数的调用方式发生了变化,通过:
CalTime(Bar)
进行调用,所以传入函数名:
- 可以实现功能
- 改变了调用方式
- 源代码未改变
然后利用高阶函数的第二个特性,返回函数名:
#函数返回的是一个函数名 import time def Bar(): time.sleep(2) print("我是bar函数") def CalTime(func): start_time=time.time() func() end_time=time.time() print( end_time-start_time) return func Bar=CalTime(Bar) Bar() #################输出######### #我是bar函数 #2.0001144409179688 #我是bar函数
从上面的输出看好像实现了装饰器的原则呀,不,细看后可以发现,调用方式没有发生变化,但是功能没有实现,它多打印了一行内容,这是由于返回函数名的原因,所以:
- 没有实现功能
- 调用方式未发生变化
- 源代码未改变
此时,可能有的人想将两个综合起来不就实现了吗?事实上结果是:
- 没有实现功能
- 调用方式未发生变化
- 源代码未改变
此时明显高阶函数不能完全解决这个问题,引入嵌套函数?
4、函数嵌套
python中定义函数的关键字是def,如果在函数中使用def关键字再创建函数,这时就会形成函数嵌套。
def f1(): name="bar" def f2(): print(name) f2() f1() #bar
像上述这样的形式属于函数嵌套,而函数中调用其它函数不属于函数嵌套,那么引入函数嵌套到底有什么作用呢?从上述的执行结果可以看到打印出name的值,f2作用域中没有就会向上一层中寻找变量,这里就是需要利用这个特性来做文章的。
import time def Bar(): time.sleep(2) print("我是bar函数") def CalTime(func): def wrapper(): start_time=time.time() func() end_time=time.time() print(end_time-start_time) return wrapper Bar=CalTime(Bar) Bar() ##############输出#### #我是bar函数 #2.0001144409179688
此时可以看到我在wrapper函数中执行了外层作用域中函数变量func,而且可以看到输出结果是正确的,这样装饰器基本实现了,可能有的人说闭包呢?函数嵌套里面已经包括闭包了,wrapper函数是一个闭包,CalTime函数也是一个闭包,当然这还不是一个完整的装饰器,不可能每一次调用方式都需要这样写吧?
Bar=CalTime(Bar)
Bar()
所以,此时引入“@”
import time def CalTime(func): def wrapper(): start_time=time.time() func() end_time=time.time() print(end_time-start_time) return wrapper @CalTime # Bar=CalTime(Bar) def Bar(): time.sleep(2) print("我是bar函数") Bar()
这样一个简单的装饰器就出来了
@CalTime 就相当于 Bar=CalTime(Bar)
可以整理一下运行过程,Bar()运行相当于CalTime(Bar)(),CalTime(Bar)()相当于执行函数wrapper(),而在wrapper函数中又执行了func函数(也就是Bar())。
二、装饰器
1、装饰带参数的函数
上面是简单的装饰器,装饰的函数是没有参数的,如果Bar()是带有参数的函数,此时又应该如何解决呢?
import time def CalTime(func): def wrapper(*args,**kwargs):#相当于Bar(1,b=2) args=(1,) kwargs={"b":2} start_time=time.time() func(*args,**kwargs) #运行Bar() func(*(1,),**{'b':18}) end_time=time.time() print(end_time-start_time) return wrapper @CalTime # Bar=CalTime(Bar) def Bar(a,b): time.sleep(2) print("我是bar函数") Bar(1,b=2) #########################输出################ #我是bar函数 #2.0001144409179688
这就是被修饰的函数加上可变的参数,这样装饰器会更加实用
2、装饰带返回值的函数
上面已经说了装饰带参数的函数,但是却没有返回值,可以由上面的代码看到执行wrapper函数后没有任何返回值,可以打印Bar函数的执行结果为None。
... res=Bar(1,b=2) print(res)#None ...
此时可以加上返回值:
import time
def CalTime(func): def wrapper(*args,**kwargs):#相当于Bar(1,b=2) args=(1,) kwargs={"b":2} start_time=time.time() res=func(*args,**kwargs) #运行Bar() func(*(1,),**{'b':18}) 使用变量res存放被装饰函数的返回 end_time=time.time() print(end_time-start_time) return res #包装函数并且返回res,实现返回值的传递 return wrapper @CalTime # Bar=CalTime(Bar) def Bar(a,b): time.sleep(2) return "我是bar函数" res=Bar(1,b=2) print(res)#None #########################输出################ #2.0001144409179688 #我是bar函数
3、functools.wraps
如何保持原函数的元信息呢?
从上面的代码如果打印:
... res=Bar(1,b=2) print(res) print(Bar.__name__) #wrapper ...
可以看到打印Bar函数名字输出wrapper,这样并没有保留原函数的元信息,此时可以利用functools.wraps达到这个效果:
import time import functools def CalTime(func): @functools.wraps(func) def wrapper(*args,**kwargs):#相当于Bar(1,b=2) args=(1,) kwargs={"b":2} start_time=time.time() res=func(*args,**kwargs) #运行Bar() func(*(1,),**{'b':18}) 使用变量res存放被装饰函数的返回 end_time=time.time() print(end_time-start_time) return res #包装函数并且返回res,实现返回值的传递 return wrapper @CalTime # Bar=CalTime(Bar) def Bar(a,b): time.sleep(2) return "我是bar函数" res=Bar(1,b=2) print(res) print(Bar.__name__) #Bar
此时一个完整的装饰器就出来了。
4、装饰器接收参数
有时针对不同的被装饰的函数,装饰器需要做一些调整,可以将装饰器传入需要的参数,完成一定的功能。
这是一般的装饰器:
import functools def fruit(func): @functools.wraps(func) def wrapper(*args,**kwargs): print("颜色:%s,价格:%s"%(args[0],args[1])) return func(*args,**kwargs) return wrapper @fruit def apple(color,price): return "我爱吃苹果" @fruit def pear(color,price): return "我爱吃梨子" apple("绿色",20)#颜色:绿色,价格:20 pear("黄色",10) #颜色:黄色,价格:10
现在需求是根据不同的爱好选择不同的水果,也就是传入不同水果类型,执行不同水果函数:
import functools def outer(type=None): def fruit(func): @functools.wraps(func) def wrapper(*args,**kwargs): print("颜色:%s,价格:%s"%(args[0],args[1])) if type=="apple": return func(*args,**kwargs) elif type=="pear": return func(*args, **kwargs) else: print("无可用函数执行!") return wrapper return fruit @outer(type="apple") #@fruit 相当于apple=fruit(apple) def apple(color,price): return "我爱吃苹果" @outer(type="pear") def pear(color,price): return "我爱吃梨子" res=apple("绿色",20)#颜色:绿色,价格:20 print(res)#我爱吃苹果 # pear("黄色",10) #颜色:黄色,价格:10
从上面可以看出,这样又加一层嵌套函数,但实际作用就是传入一个type值,其它的还是没变:
@outer(type="apple")----------》@fruit------------》相当于apple=fruit(apple)
这也是对装饰器的进一步扩展了。
三、实例
1、登陆验证
一般web页面登陆后需要记录登录后用户的信息,这样进入其它页面就不用再次登陆了,这里模拟一下登陆认证,首先可以写一个登陆的处理函数,如果登陆成功后,将用户的的信息写入session中:
def login(request): if request.method=="GET": return render(request,'login.html') username=request.POST.get("username") password=request.POST.get("password") user=models.UserInfo.objects.filter(username=username,password=password).first() if user: request.session['user_info'] = user return redirect("/index/")
写一个装饰器,进入主页后的登陆判断:
def check_login(func): def wrapper(request,*args,**kwargs): if request.session['user_info']: return func(request,*args,**kwargs) else: return redirect("/login/") return wrapper
再进入其它页面使用这个装饰器:
@check_login def index(request): return render(request,"index.html")
2、登陆验证类型的选择
上面使用的session进行判断的,当然,还可以给装饰器加上参数,选择不同的验证方式,只需要做简单的修改:
def auth_type(type=None): def check_login(func): def wrapper(request,*args,**kwargs): if type == "file": #从文件中取出用户名和密码进行校验 print("用文件的方式进行验证") if request.POST.get("username") == "xxx" and request.POST.get("passworld") == "yyy": return func(request, *args, **kwargs) else: return redirect("/login/") else: if request.session['user_info']: return func(request,*args,**kwargs) else: return redirect("/login/") return wrapper return check_login
其它的函数被装饰:
@auth_type(type="file") def home(request): return render(request,"home.html") @auth_type def index(request): return render(request,"index.html")