Fork me on GitHub

装饰器

一、装饰器基础

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
加入functools.wraps

 此时一个完整的装饰器就出来了。

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/")
login

写一个装饰器,进入主页后的登陆判断:

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")

 

posted @ 2019-06-17 11:10  iveBoy  阅读(387)  评论(0编辑  收藏  举报
TOP