python重難點之裝飾器詳解

背景

雖然之前看過裝飾器的相關內容,但是今天想起來,一直沒有好好總結一下,所以特地記錄下關於裝飾器的一系列用法。
要想理解裝飾器首先要明確頗python中的三個概念:
1.一切函數皆爲對象
2.高階函數
3.嵌套函數
然後才能理解:
4.什麼是裝飾器?
5.裝飾器如何實現?
6.裝飾器有什麼用?

詳細解釋

一切函數皆爲對象

準確來說在Python,一切皆爲對象,此處說的點與函數相關所以將範圍縮小了一下。看一個最基本的例子,我們可以發現我們可以直接將test1像變量一樣賦值給a,然後a可以像函數一樣使用。

def test1():
    print('i am test1')

# 可將test1想變量一樣賦值給a,然後a便可以像函數一樣使用
a = test1
a()
# output:
# i am test1

高階函數

函數參數可以接受變量,那麼一個函數可以接受另一個函數作爲參數,或者返回值爲參數(這個稍後再說),那這種函數稱之爲高階函數,如下面這段代碼:

def add (a, b, f):
    return f(a) + f(b)
res = add(3, -6, abs)
print(res)

在這段代碼代碼中,add()就是一個高階函數,可以看見在add()中的參數中有將abs()這個取絕對值函數做爲參數。

嵌套函數

在一個函數的函數體內用def去申明一個函數,這樣的函數叫做嵌套函數,如下面這段代碼:

def foo():
    print('in the foo')
    def bar():
        print('in the bar')
    bar()
foo()
# outputs:
# in the foo
# in the bar

通過調用foo(),將在foo()內部定義並且調用bar()。我們是稍微改動一下代碼:

def foo():
    print('in the foo')
    def bar():
        print('in the bar')
    return bar # 將bar作爲foo()的返回值
a = foo()

上面這段代碼稍微改了一下foo的返回值,即將bar作爲foo的返回值返回了,還記得上面所說的高階函數的定義麼?這就是將函數返回的類型,然後我們將foo()賦值給變量a,這不就是我們呢所說的函數即變量的概念麼?
最後得到輸出:

in the foo

如果我們在後面再加一句:

a()

將會輸出:

in the bar

這表明現在這個a已經是個函數類型了,他的功能就是foo()函數裏面bar()的功能。

什麼是裝飾器?

說了這麼多鋪墊,那到底什麼是裝飾器呢?
其實裝飾器的本質還是函數,它是爲了裝飾其他函數的,說白了就是爲其他函數添加附加功能的
具體什麼意思呢?比如我們我們之前寫了一個函數,我們現在想在這個函數上添加一些功能,但是我們又不能改變在原來的函數基本上修改,而且還不能修改它的調用方式,因爲它可能在很多地方已經被調用了,所以我們就必須要搞一個裝飾器,來給原來的函數裝飾一下,便於實現新的功能。
那裝飾器應該怎麼弄?我用一個公式來概括一下:
高階函數 + 嵌套函數 —-> 裝飾器
即通過高階函數和嵌套函數我們就可以實現一個裝飾器

如何實現一個裝飾器

假設我們有一個函數func()如下,我現在想知道這個函數總共運行了多長時間,並且打印出來,而且我不能修改func()本身,並且不可以該變它的調用方式,那怎麼辦?

import time

def func():
    time.sleep(2) #模擬一系列的操作
    print('i am func in 1')

我們經過思考,結合上文可以寫下如下的函數:

def func_time(func):
    start_time = time.time()
    func()
    end_time = time.time()
    print('func time is ',end_time - start_time)

func_time(func) 
# outputs:
# i am func in 1
# func time is  2.0014798641204834

我們可以發現func_time()是可以統計func()的運行時間的,但是我想要的結果是直接使用func()就可以出現這樣的效果,而且以後每次這麼用,都會有這樣的效果啊。
於是我們想起一切函數皆爲對象,以及嵌套函數、高階函數的用法,再改一改,得到如下的代碼:

def func_time(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print('func time is ',end_time - start_time)
    return wrapper

func = func_time(func)
func()

我們在func_time()中使用嵌套函數定義了一個wrapper()函數,然後將剛纔的操作都放在這個wrapper()中了,最後我們將wrapper作爲func_time()的返回值返回了,在函數外面我們將func_time(func)又賦值給了func(),即將wrapper賦值給了func(),也就是說func()其實是實現的wrapper()的功能,而在wrapper中不但有func()的功能而且還有計算func()運行時間的功能。最後我們每次調用func()就會實現計時的功能了。
到這裏其實我們已經手動的實現了python的裝飾器功能了,但是有沒有更簡單的方法呢?是有的,在python中提供了一個裝飾器語法,在上面的這個例子中,我們只需在func()函數前面加上一句@func_time。
@ 符號就是裝飾器的語法糖,它放在函數開始定義的地方,這樣就可以省略最後一步再次賦值的操作。什麼是語法糖?就是計算機添加的某種語法,對語言的功能沒有影響,但方便程序員使用
然後我們直接就可以使用func()即可,完整的也就是:

@func_time
def func():
    time.sleep(2)
    print('i am func in 1')

也就是說@func_time其實就是等價於

func = func_time(func)

還有一點需要注意的是func_time()這個函數的定義一定要寫在func()定義前面,要不然使用@func_time,python在內存中是找不到func_time()的位置的。
完整的代碼是這樣的:

import time

def func_time(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print('func time is ',end_time - start_time)
    return wrapper

@func_time
def func():
    time.sleep(2)
    print('i am func in 1')

func()

到此你就實現了一個基本的裝飾器,但是如果你還不滿足,請繼續向下看。

————————————-華麗的分割線——————————————

如何實現一個裝飾器(進階)

裝飾器還可以怎麼用?首先第一個就是裝飾器可以累計使用
現在我有一個函數:

def say():
   return "Hello"

我希望它可以根據不同的需要實現以下兩種輸出,不定時切換:

<b><i>Hello</i></b>
<i><b>Hello</b></i>

我們可以用裝飾器很輕易的實現,我們先實現兩個裝飾器:

# 用來裝飾say()產生<b></b>
def makebold(fn):
    def wrapper():
        return "<b>" + fn() + "</b>"
    return wrapper

# 用來裝飾say()產生<i></i>
def makeitalic(fn):
    def wrapper():
        return "<i>" + fn() + "</i>"
    return wrapper

然後我們來裝飾say():

@makebold
@makeitalic
def say():
    return "hello"

print(say())

通過上面的裝飾我們可以輸出:

<b><i>hello</i></b>

如果我們將兩個裝飾器的位置改變一下即:

@makeitalic
@makebold
def say():
    return "hello"

print(say())

我們就可以輸出:

<i><b>hello</b></i>

如何實現一個裝飾器(高階)

我們應該如何給一個被修飾的函數傳遞參數呢?
其實當你調用被裝飾器返回的函數時,實際你是在調用封裝函數 ,向封裝函數傳遞參數可以讓封裝函數把參數傳遞給被裝飾的函數。
我們先來看看在最開始統計函數運行時間的小程序上做的一點改變後的樣子:

def count(func):
    def sleep_time(name): # 傳入參數
        start_time = time.time()
        func(name)
        stop_time = time.time()
        print(stop_time - start_time)
    return sleep_time

@count
def sleep2(name):
    time.sleep(2)
    print(name, 'sleepping in 2') #打印參數

sleep2('sty')
# outputs:
# sty sleepping in 2
# 2.002162218093872

我們可以看到’sty’已經作爲參數傳進入了,但是如果我們有的時候我們對有的被裝飾函數我們不需要參數,或者我們不確定有多少個參數該怎麼辦?我們總不能再重複的寫裝飾器吧?這個時候我們就需要使用到args,kwargs了,在python中參數有四類:必選參數、默認參數、可變參數和關鍵字參數,這4種參數都可以一起使用,或者只用其中某些,但是請注意,參數定義的順序必須是:必選參數、默認參數、可變參數和關鍵字參數*,關於具體的詳細介紹點這裏

*args:接受N個位置參數,轉換爲元組的形式
**kwargs:接受N個位置參數,轉換爲字典的形式

這樣我們就可以對我們的裝飾器進行一定的改進了,如下面的代碼:

import time

def count(func):
    def sleep_time(*args,**kwargs):
        start_time = time.time()
        func(*args,*kwargs)
        stop_time = time.time()
        print(stop_time - start_time)
    return sleep_time
@count
def sleep1():
    time.sleep(2)
    print('i am sleepping in 1')

@count
def sleep2(name):
    time.sleep(2)
    print(name, 'sleepping in 2')


sleep1()
sleep2('sty')
# outputs:
# i am sleepping in 1
# 2.0013535022735596
# sty sleepping in 2
# 2.0010647773742676

在這段代碼中我們利用裝飾器裝飾了兩個函數sleep()和sleep1(),sleep()沒有傳參數,sleep1()傳了一個參數,發現都可以完好運行。

一個問題需要引起你的注意:
在上面的代碼最後添加下面的兩行,我們打印下sleep1()和sleep2()的名字

print('sleep1 name is :', sleep1.__name__)
print('sleep2 name is :', sleep2.__name__)

得到的結果是:

sleep1 name is : sleep_time
sleep2 name is : sleep_time

雖然我們知道sleep1()和sleep2()就是執行的sleep_time()的功能,但是我們還是不願意看見它的真實名字改變。這並不是我們想要的!我們希望的結果輸出應該是:

sleep1 name is : sleep1
sleep2 name is : sleep2

這裏的函數被sleep_time()替代了。它重寫了我們函數的名字和註釋文檔(docstring)。幸運的是Python提供給我們一個簡單的函數來解決這個問題,那就是functools.wraps。我們修改一下上例, 在封裝函數前加上@wraps(func),可以得到:

from functools import wraps
import time

def count(func):
    @wraps(func)    #在封裝函數前加上
    def sleep_time(*args,**kwargs):
        start_time = time.time()
        func(*args,*kwargs)
        stop_time = time.time()
        print(stop_time - start_time)
    return sleep_time
@count
def sleep1():
    time.sleep(2)
    print('i am sleepping in 1')

@count
def sleep2(name):
    time.sleep(2)
    print(name, 'sleepping in 2')


sleep1()
sleep2('sty')
print('sleep1 name is :', sleep1.__name__)
print('sleep2 name is :', sleep2.__name__)
#Outputs:
# i am sleepping in 1
# 2.0020651817321777
# sty sleepping in 2
# 2.002112627029419
# sleep1 name is : sleep1
# sleep2 name is : sleep2

裝飾器實戰之授權認證

需求:假如現在一個網站有三個頁面,index, home, bbs, 其中index頁面是你不需要登錄就可以查看的,而home,bbs是需要登錄才能查看的,並且告訴我登錄授權形式,有local和remote兩種可以選擇,我們應該怎麼做?

解決方法:裝飾器高階用法,需要給裝飾器一開始就傳參數,需要在裝飾器中再寫一個函數傳遞被裝飾函數。說的可能有點繞,具體查看下面的代碼:

from functools import wraps

user, passwd = 'sty', '1234'  # 定義一個默認的用戶名和密碼
def auth(auth_type):
    print('now auth_type is:', auth_type)
    def outer_wrapper(func): # 又設了一層函數,來傳遞被裝飾函數的
        @wraps(func)
        def wrapper(*args, **kwargs):
            user_name = input("UserName:").strip()
            pass_word = input("PassWord:").strip()

            if user == user_name and passwd == pass_word:
                print("\033[32;1mUser has passed authentication\033[0m")  #讓打印帶顏色顯示
                func(*args, **kwargs)
            else:
                print("\033[31;1mInvalid username or passward\033[0m")
        return wrapper  
    return outer_wrapper

def index():
    print("welcome to the index page")

@auth(auth_type='local') #等價於home = auth(auth_type='local')(home)
def home():
    print("welcome to the home page")

@auth(auth_type='remote') #等價於home = auth(auth_type='ldap')(home)
def bbs():
    print("welcome to the bbs page")

index()
print('login in home, input name and password:')
home()
print('login in bbs: input name and password:')
bbs()

我們注意到:
@auth(auth_type=’local’) 等價於home = auth(auth_type=’local’)(home)
@auth(auth_type=’remote’) 等價於home = auth(auth_type=’ldap’)(home)
當這樣看的時候我們就不難理解它是如何實現的了
到此裝飾器的幾乎大部分功能就差不多了,其他的一些具體用法還需要你逐漸去探索

最後提一句

在學習裝飾器的時候,如果你還想從更加深入的去理解,那你就得需要知道’閉包’,關於什麼是閉包?以及它和裝飾器的關係?有時間將在接下來的博客中提到。

參考文檔:

https://stackoverflow.com/questions/739654/how-to-make-a-chain-of-function-decorators
https://stackoverflow.com/questions/13857/can-you-explain-closures-as-they-relate-to-python
https://segmentfault.com/a/1190000004461404
http://www.cnblogs.com/itech/archive/2011/12/31/2308640.html
http://www.cnblogs.com/vamei/archive/2012/12/15/2772451.html

轉載請註明出處:
CSDN:樓上小宇_home:http://blog.csdn.net/sty945
簡書:樓上小宇:http://www.jianshu.com/u/1621b29625df

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章