七:python中的裝飾器

1.概念:
裝飾器是一個很著名的設(shè)計(jì)模式,經(jīng)常被用于有切面需求的場景,較為經(jīng)典的應(yīng)用有插入日志、增加計(jì)時邏輯來檢測性能、加入事務(wù)處理等。裝飾器是解決這類問題的絕佳設(shè)計(jì),有了裝飾器,我們就可以抽離出大量函數(shù)中與函數(shù)功能本身無關(guān)的雷同代碼并繼續(xù)重用。概括的講,裝飾器的作用就是為已經(jīng)存在的對象添加額外的功能。

  1. 裝飾器應(yīng)用的由來:
    裝飾器的定義很是抽象,我們來看一個小例子。
    先定義一個簡單的函數(shù):

def foo():
print 'in foo()'
foo()
然后呢,我想看看執(zhí)行這個函數(shù)用了多長時間,好吧,那么我們可以這樣做:
import time
def foo():
start = time.clock()
print 'in foo()'
end = time.clock()
print 'used:', end - start

foo()
很好,功能看起來無懈可擊。但是如果我要看看另外一個函數(shù)的執(zhí)行性能,計(jì)算時間的這些與函數(shù)本身功能無關(guān)的代碼我還得寫一遍,這很不軟件工程啊,怎么辦呢?
還記得嗎,函數(shù)在Python中是一等公民,那么我們可以考慮重新定義一個函數(shù)timeit,將foo的引用傳遞給他,然后在timeit中調(diào)用foo并進(jìn)行計(jì)時,這樣,我們就達(dá)到了不改動foo定義的目的
import time

def foo():
print 'in foo()'

def timeit(func):
start = time.clock()
func()
end =time.clock()
print 'used:', end - start

timeit(foo)
看起來邏輯上并沒有問題,一切都很美好并且運(yùn)作正常!但是,我們似乎修改了調(diào)用部分的代碼。原本我們是這樣調(diào)用的:foo(),修改以后變成了:timeit(foo)。這樣的話,如果foo在N處都被調(diào)用了,你就不得不去修改這N處的代碼?;蛘吒鼧O端的,考慮其中某處調(diào)用的代碼無法修改這個情況,比如:這個函數(shù)是你交給別人使用的。
既然如此,我們就來想想辦法不修改調(diào)用的代碼;如果不修改調(diào)用代碼,也就意味著調(diào)用foo()需要產(chǎn)生調(diào)用timeit(foo)的效果。我們可以想到將timeit賦值給foo,但是timeit似乎帶有一個參數(shù)……想辦法把參數(shù)統(tǒng)一吧!如果timeit(foo)不是直接產(chǎn)生調(diào)用效果,而是返回一個與foo參數(shù)列表一致的函數(shù)的話……就很好辦了,將timeit(foo)的返回值賦值給foo,然后,調(diào)用foo()的代碼完全不用修改!
import time

def foo():
print 'in foo()'
定義一個計(jì)時器,傳入一個,并返回另一個附加了計(jì)時功能的方法
def timeit(func):

# 定義一個內(nèi)嵌的包裝函數(shù),給傳入的函數(shù)加上計(jì)時功能的包裝
def wrapper():
    start = time.clock()
    func()
    end =time.clock()
    print 'used:', end - start

# 將包裝后的函數(shù)返回
return wrapper

foo = timeit(foo)#傳入的參數(shù)foo為原函數(shù)名,變量foo為新變量不是原函數(shù)
foo()
這樣,一個簡易的計(jì)時器就做好了!我們只需要在定義foo以后調(diào)用foo之前,加上foo = timeit(foo),就可以達(dá)到計(jì)時的目的,這也就是裝飾器的概念,看起來像是foo被timeit裝飾了。
這樣,一個簡易的計(jì)時器就做好了!我們只需要在定義foo以后調(diào)用foo之前,加上foo = timeit(foo),就可以達(dá)到計(jì)時的目的,這也就是裝飾器的概念,看起來像是foo被timeit裝飾了。

在這個例子中,函數(shù)進(jìn)入和退出時需要計(jì)時,這被稱為一個橫切面(Aspect),這種編程方式被稱為面向切面的編程(Aspect-Oriented Programming)。與傳統(tǒng)編程習(xí)慣的從上往下執(zhí)行方式相比較而言,像是在函數(shù)執(zhí)行的流程中橫向地插入了一段邏輯。在特定的業(yè)務(wù)領(lǐng)域里,能減少大量重復(fù)代碼。面向切面編程還有相當(dāng)多的術(shù)語,這里就不多做介紹,感興趣的話可以去找找相關(guān)的資料。

這個例子僅用于演示,并沒有考慮foo帶有參數(shù)和有返回值的情況,完善它的重任就交給讀者你最為練習(xí) ,可以檢驗(yàn)下你是否真正理解了學(xué)會了。
上面這段代碼看起來似乎已經(jīng)不能再精簡了,Python于是提供了一個語法糖來降低字符輸入量
import time

def timeit(func):
def wrapper():
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
return wrapper

@timeit
def foo():
print 'in foo()'

foo()
重點(diǎn)關(guān)注第11行的@timeit,在定義上加上這一行與另外寫foo = timeit(foo)完全等價(jià),千萬不要以為@有另外的魔力。除了字符輸入少了一些,還有一個額外的好處:這樣看上去更有裝飾器的感覺。
函數(shù)引用;
要理解python的裝飾器,我們首先必須明白在Python中函數(shù)也是被視為對象。這一點(diǎn)很重要。先看一個例子:
def shout(word="yes") :
return word.capitalize()+" !"

print shout()

輸出 : 'Yes !'

作為一個對象,你可以把函數(shù)賦給任何其他對象變量

scream = shout

注意我們沒有使用圓括號,因?yàn)槲覀儾皇窃谡{(diào)用函數(shù)

我們把函數(shù)shout賦給scream,也就是說你可以通過scream調(diào)用shout

print scream()

輸出 : 'Yes !'

還有,你可以刪除舊的名字shout,但是你仍然可以通過scream來訪問該函數(shù)

del shout
try :
print shout()
except NameError, e :
print e
#輸出 : "name 'shout' is not defined"

print scream()

輸出 : 'Yes !'

我們暫且把這個話題放旁邊,我們先看看python另外一個很有意思的屬性:可以在函數(shù)中定義函數(shù):
def talk() :

# 你可以在talk中定義另外一個函數(shù)
def whisper(word="yes") :
    return word.lower()+"...";

# ... 并且立馬使用它

print whisper()

你每次調(diào)用'talk',定義在talk里面的whisper同樣也會被調(diào)用

talk()

輸出 :

yes...

但是"whisper" 不會單獨(dú)存在:

try :
print whisper()
except NameError, e :
print e
#輸出 : "name 'whisper' is not defined"*
從以上兩個例子我們可以得出,函數(shù)既然作為一個對象,因此:
其可以被賦給其他變量

其可以被定義在另外一個函數(shù)內(nèi)

這也就是說,函數(shù)可以返回一個函數(shù),看下面的例子:
def getTalk(type="shout") :

# 我們定義另外一個函數(shù)
def shout(word="yes") :
    return word.capitalize()+" !"

def whisper(word="yes") :
    return word.lower()+"...";

# 然后我們返回其中一個
if type == "shout" :
    # 我們沒有使用(),因?yàn)槲覀儾皇窃谡{(diào)用該函數(shù)
    # 我們是在返回該函數(shù)
    return shout
else :
    return whisper

然后怎么使用呢 ?

把該函數(shù)賦予某個變量

talk = getTalk()

這里你可以看到talk其實(shí)是一個函數(shù)對象:

print talk

輸出 : <function shout at 0xb7ea817c>

該對象由函數(shù)返回的其中一個對象:

print talk()

或者你可以直接如下調(diào)用 :

print getTalk("whisper")()

輸出 : yes...

<p></p>
還有,既然可以返回一個函數(shù),我們可以把它作為參數(shù)傳遞給函數(shù):
def doSomethingBefore(func) :
print "I do something before then I call the function you gave me"
print func()

doSomethingBefore(scream)

輸出 :

I do something before then I call the function you gave me

Yes !

這里你已經(jīng)足夠能理解裝飾器了,其他它可被視為封裝器。也就是說,它能夠讓你在裝飾前后執(zhí)行代碼而無須改變函數(shù)本身內(nèi)容。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容