天天看點

5.初識python裝飾器 高階函數+閉包+函數嵌套=裝飾器

一.什麼是裝飾器?

實際上裝飾器就是個函數,這個函數可以為其他函數提供附加的功能。

裝飾器在給其他函數添加功能時,不會修改原函數的源代碼,不會修改原函數的調用方式。

高階函數+函數嵌套+閉包 = 裝飾器

1.1什麼是高階函數?

1.1.1函數接收的參數,包涵一個函數名。

1.1.2 函數的傳回值是一個函數名。

其實這兩個條件都很好滿足,下面就是一個高階函數的例子。

def test1():

    print "hamasaki ayumi" 

def test2(func):

    return test1

下面這個代碼就滿足了對高階函數所有的條件,

1.1.3 滿足以上條件的任意一個條件,就可以稱為高階函數。

如果隻是給一個函數添加一個功能,并且不修改原函數的代碼,這一點需求,我們通過高階函數就可以實作,下面是給函數添加一個計算執行時間并不修改函數原有代碼的功能。

#!/usr/bin/python2.7

# -*- coding:utf-8 -*-

import time

def foo():

    time.sleep(1)

    print "hamasaki ayumi <<a best>> 3.28 now on sale!"

def test(func):

    start_time = time.time()

    func()

    stop_time = time.time()

    run_time = stop_time - start_time

    print run_time

test(foo)

輸出結果:

hamasaki ayumi <<a best>> 3.28 now on sale!

1.00331807137

通過上面的測試結果,可以看出,沒有修改foo函數的原代碼,還給函數增加了一個顯示執行時間的功能。

雖然使用高階函數給原函數foo增加了功能,但是修改了原函數的調用方式,違反了開放封閉原則。

剛剛說了高階函數接收的參數是一個函數名,我們可以利用這個特性,給函數增加功能(在不修改原代碼的前提下)。

那麼,高階函數的另外一個特點,傳回值是一個函數,如果應用這種特性,就可以做到不改變函數的調用方式了。那我們就來試一下,隻通過高階函數,是否可以完成裝飾器的功能。

def runtime(func):

    return func

foo = runtime(foo)

foo()

輸出結果如下:

1.0050868988

從得到的結果來看,foo函數被多執行了一次,具體原因,來分析下。

首先調用了runtime(foo),解釋器解釋到func()的時候,foo函數被執行了一次,runtime函數在return的時候,将foo函數體,return給了foo變量,最後我們在調用foo()的時候,導緻foo函數又被多執行了一次,這顯然不是我們想要的效果。

是以說,“裝飾器”所擁有的功能,單單隻通過高階函數,是無法實作的。

接下來就需要用到剩下的兩個知識點了,分别是函數嵌套和函數閉包。

2.什麼是函數嵌套?

說到函數嵌套,總會有人以為,在一個函數中調用了另外一個函數,就屬于函數嵌套了,就像下面這個例子一樣。

def f1():

    print 'f1'

def f2():

    print 'f2'

    f1()

注意!!這并不是函數嵌套!!

真正的函數嵌套是指,在一個函數中又定義了一個函數。

    def f2():

        print 'f2'

    print locals()

f1()

像這種在一個函數體内又建立了一個函數,這種形式才屬于函數嵌套。

3.什麼是閉包函數?

閉包函數,大概可以解釋為,在一個内部函數中,對外部作用域(這裡的外部作用域指的不是全局作用域!)的變量進行引用,函數内部就可以被了解為是閉包的,下面是一個閉包函數的例子。

a = 1

    a = 2

        print a

    f2()

3.1 關于閉包函數的一些使用注意事項。

3.1.1 第一條要注意的就是!閉包函數内部,預設是不可以修改外部作用域的變量的!!(使用nonlocal關鍵字聲明例外)

示例1:

    x = 1

        x = 2

        print x

    print x

最後輸出的結果是:

1

2

在閉包函數中定義的x變量,并沒有影響到外部作用域的x變量。

例子2:

    a = 1

    def bar():

        a = a + 1

        return a

    return bar

f = foo()

print f()

分析一下這段代碼所存在的問題,在執行foo()函數的時候,python會倒入閉包函數bar,來分析這個作用域的局部變量,其實python在内部規定了,在等号左邊的變量都是局部變量,在閉包函數bar中,a指派在等号的左邊,被python認定為這個a是閉包函數bar中的局部變量,在執行print f()時,程式運作到a=a+1的時候,python會在閉包函數bar中找在等号右邊的a的值,如果找不到,就報錯了。(因為python之前已經把a當做bar閉包函數中的局部變量了。)

那麼接下來,我們融合高階函數+閉包+函數嵌套這三個知識點,來試試看是否可以實作裝飾器的功能。

    def func_in():

        start_time = time.time()

        func()

        stop_time = time.time()

        run_time = stop_time - start_time

        print run_time

    return func_in

輸出的結果:

1.00491380692

從結果中可以看出,這次通過上面說的三個知識點,成功初步實作了“裝飾器”的功能。

雖然這個通過函數閉包,函數嵌套,高階函數這三個函數的特性實作了類似裝飾器的功能,但是,還是有個小瑕疵,我們看下上個例子的最後兩行代碼。

如果有很多個函數都要使用前面這個“裝飾器”的話,每個函數在調用之前都要重新指派特别麻煩!

為了更完美的實作裝飾器的功能,還需要引入python中的“文法糖”,也就是一個@符号。

這個“@”符号就是python裝飾器的文法糖,用法就是@後面加上裝飾器的名字,需要使用這個裝飾器,“裝飾”哪個函數,就把這個裝飾器加在函數的上面就可以了。下面是裝飾器文法糖的使用示例。

@runtime

#使用rumtime裝飾器,對foo函數做“裝飾”。

@runtime = foo = runtime(foo)

這兩個文法意義是相同的,隻不過不用在每次調用函數之前,都要對函數重新指派。

4.裝飾器的傳回值。

這隻是實作了基本的裝飾器,看上面的例子,會發現一個問題,在執行foo函數的時候沒有傳回值。

現在有個需求,就是給foo函數添加個傳回值,那我們就先直接在foo函數中retrun一個傳回值出來,看看會有什麼效果。

    return "此處是foo函數的傳回值"

a = foo() #将foo函數的傳回值指派給變量a,然後輸出變量a的結果,看看是否能得到我們想要的傳回值。

print a

1.00362682343

None

明明在foo函數中定義了傳回值,但是執行了foo函數所獲的的傳回值依舊是None,這是為什麼?

下面來分析一下原因。

首先,foo函數使用了之前寫的runtime裝飾器@runtime就相當于foo = runtime(foo),看起來運作的是foo函數但實際運作的是runtime函數,runtime函數中,内部包涵的子函數func_in(其實準确的說,最終執行的是runtime函數中的func_in子函數),沒有包含任何傳回值,如果想讓這個foo函數能return出指定的傳回值,就需要從上一層的裝飾器開始下手。

        ret = func()   #注意看這裡!!這裡将傳進來的foo函數的傳回值賦給了ret變量

        return ret   #注意看這裡!!這裡将ret變量直接傳回了出去

#foo = runtime(foo)

print foo()

下面是輸出結果:

1.00427913666

此處是foo函數的傳回值

現在通過修改runtime函數内部的func_in子函數的傳回值,得到了想要的結果。

5.裝飾器的變量

依舊用前面的例子舉例,現在foo函數中定義了兩個形參,需要接收兩個參數才可以運作,首先,我們修改foo函數。

def foo(name,age):

    print "name:%s age:%s" %(name,age)

下面來調用foo函數來看結果。

print foo(name='suhaozhi',age=18)

    print foo(name='suhaozhi',age=18)

TypeError: func_in() takes no arguments (2 given)

結果就是傳回了一個異常。

我們來看下這個異常的内容,說的是func_in函數不需要任何參數,其實仔細想想我們執行foo函數,後 main真正執行的就是裝飾器runtime内部定義的func_in子函數,是以,要給原來的函數傳什麼樣的值,在裝飾器内部也要做相應的修改。

    def func_in(*args,**kwargs):    《====#注意看這裡!當調用foo函數時,其實時從這裡開始執行。

        ret = func(*args,**kwargs)  《=====#注意看這裡,此處用于接收func_in傳進來的參數。

        return ret

下面時輸出結果。

name:suhaozhi age:18

1.00473690033

至于為什麼要使用萬能參數,就是因為裝飾器要修飾的每個函數所傳的變量都是不一樣的,是以使用萬能參數後,裝飾器内部就可以接收各種不同類型不同個數的變量了。

本篇文章隻是對裝飾器的初步了解!關于裝飾器的更多内容都在後面的文章中~未完待續~

      本文轉自蘇浩智 51CTO部落格,原文連結:http://blog.51cto.com/suhaozhi/1909347,如需轉載請自行聯系原作者