天天看點

裝飾器

一、裝飾器基礎

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

作者:iveBoy

出處:http://www.cnblogs.com/shenjianping/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須在文章頁面給出原文連接配接,否則保留追究法律責任的權利。