python中的裝飾器用于修飾函數(shù),以增強(qiáng)函數(shù)的行為:記錄函數(shù)執(zhí)行時(shí)間,建立和撤銷環(huán)境,記錄日志等。裝飾器可以在不修改函數(shù)內(nèi)部代碼的前提下實(shí)現(xiàn)以上增強(qiáng)行為。如下代碼建立一個(gè)計(jì)時(shí)裝飾器,隨后描述其工作原理。import time
def timethis(func):
def inner(*args,**kwargs):
print('start timer:')
start = time.time()
result = func(*args,**kwargs)
end = time.time()
print('end timer:%fs.'%(end - start))
return result
return inner
@timethis
def sleeps(seconds):
print(' sleeps begin:')
time.sleep(seconds)
print(' sleep %d seconds.\n sleeps over.'%seconds)
return seconds
print(sleeps(3))
start timer:
sleeps begin:
sleep 3 seconds.
sleeps over.
end timer:3.002512s.
3
可見,timethis裝飾器實(shí)現(xiàn)了為sleeps函數(shù)計(jì)時(shí)的功能。其關(guān)鍵在于@標(biāo)識(shí)符的使用。一、理解@標(biāo)識(shí)符
@標(biāo)識(shí)符是Pyton的語法糖,定義被裝飾函數(shù)時(shí)使用@timethis修飾和用語句sleeps = timethis(sleeps)是等價(jià)的。@timethis
def sleeps(seconds):
print(' sleeps begin:')
time.sleep(seconds)
print(' sleep %d seconds.\n sleeps over.'%seconds)
return seconds
def sleeps(seconds):
print(' sleeps begin:')
time.sleep(seconds)
print(' sleep %d seconds.\n sleeps over.'%seconds)
return seconds
sleeps = timethis(sleeps)
@語法只是裝飾器調(diào)用的便捷方式:將被裝飾函數(shù)sleeps作為參數(shù)傳給裝飾器函數(shù),再將裝飾器返回值重新綁定到原sleeps變量上。理解了裝飾器的使用方法,我們一步步來理解其定義過程。二、裝飾器是一個(gè)函數(shù)
- 根據(jù)上文timethis裝飾器的定義,它毫無疑問是一個(gè)函數(shù)。名稱是timethis,參數(shù)是func,返回值是inner。
- 根據(jù)
sleeps = timethis(sleeps),可知參數(shù)func是被裝飾的函數(shù)sleeps。 - 根據(jù)
return inner,可知返回值inner是嵌套定義在裝飾器中的一個(gè)函數(shù)。
綜上,裝飾器本身是一個(gè)函數(shù),參數(shù)也是函數(shù),返回值還是函數(shù)。之所以函數(shù)可以作為裝飾器的參數(shù)和返回值,是因?yàn)楹瘮?shù)在Python中是一等對(duì)象。三、函數(shù)是一等對(duì)象
編程語言中的一等對(duì)象定義為:運(yùn)行時(shí)創(chuàng)建,可賦值給變量或數(shù)據(jù)結(jié)構(gòu),可作為參數(shù)傳遞,可作為返回值返回。Python中整數(shù)、字符串、字典類型是一等對(duì)象,具備以上四點(diǎn)特性,理解起來沒有任何困難。但函數(shù)作為一等對(duì)象,需要我們舉例說明。3.1運(yùn)行時(shí)創(chuàng)建
在Python控制臺(tái)中定義一個(gè)函數(shù)reverse,實(shí)現(xiàn)對(duì)word這個(gè)序列類型的反轉(zhuǎn)。>>> def reverse(word):
... return word[::-1]
...
>>> reverse
<function reverse at 0x027A4C40>
>>> reverse('hello world!')
'!dlrow olleh'
因其是在控制臺(tái)會(huì)話中定義的,符合第一條運(yùn)行時(shí)創(chuàng)建的要求。3.2可賦值給變量或數(shù)據(jù)結(jié)構(gòu)
可以將reverse函數(shù)賦值給另外的變量,再調(diào)用。如>>> backward=reverse
>>> backward('hello world!')
'!dlrow olleh'
輸出結(jié)果同上。所以函數(shù)符合第二條可賦值給變量的要求。3.3函數(shù)作為參數(shù)傳遞
當(dāng)使用高階函數(shù),如sorted時(shí),高階函數(shù)的key關(guān)鍵字接受一個(gè)單參數(shù)函數(shù),對(duì)每個(gè)元素進(jìn)行迭代,依照這個(gè)key函數(shù)作為排序依據(jù)。cars = ['Honda','toyota','hyundai','byd','ford','suzuki','peuguot','nissan','citroen','kia','vw','gm','audi','bmw','beniz']
print(sorted(cars,key=reverse))
['Honda', 'kia', 'toyota', 'ford', 'byd', 'hyundai', 'audi', 'suzuki', 'gm', 'nissan', 'citroen', 'peuguot', 'bmw', 'vw', 'beniz']
此時(shí)所有的car是依照結(jié)尾字符的先后排序的。reverse作為參數(shù)傳入高階函數(shù)。符合第三條函數(shù)可作為參數(shù)傳遞。3.4函數(shù)作為返回值返回
為驗(yàn)證第四點(diǎn),我們將reverse函數(shù)包裝起來,讓他在一個(gè)函數(shù)中返回。def cmpLib():
def reverse(word):
return word[::-1]
return reverse
我們?nèi)杂蒙侠信判蚝瘮?shù),key參數(shù)必須為一個(gè)單參函數(shù)。而函數(shù)backward的執(zhí)行結(jié)果是一個(gè)函數(shù),所以我們把它的調(diào)用結(jié)果作為key值。print(sorted(cars,key=cmpLib())
['Honda', 'kia', 'toyota', 'ford', 'byd', 'hyundai', 'audi', 'suzuki', 'gm', 'nissan', 'citroen', 'peuguot', 'bmw', 'vw', 'beniz']
可見,結(jié)果正確。所以第四條函數(shù)可作為結(jié)果返回也成立。綜上,函數(shù)是一等對(duì)象。除了可調(diào)用性之外,函數(shù)和其他如字典、字符串、列表對(duì)象并沒有本質(zhì)區(qū)別。理解裝飾器我們需要的是函數(shù)一等性定義的后三點(diǎn):函數(shù)可賦值,可作參數(shù),可作返回結(jié)果。我們?cè)賮矸治雠c@timethis等價(jià)的sleeps = timethis(sleeps)語句:右側(cè)函數(shù)先調(diào)用。timethis是裝飾器函數(shù),被裝飾函數(shù)sleeps作為參數(shù)傳入裝飾器中;返回結(jié)果是裝飾器中定義的inner函數(shù);右側(cè)計(jì)算結(jié)果重新賦值給變量sleeps。完美符合以上三點(diǎn)。也就是說sleeps函數(shù)實(shí)際上已經(jīng)指向inner函數(shù)了。理解了函數(shù)一等性,就理解了函數(shù)可以作為參數(shù)傳遞和作為結(jié)果返回。那么新定義的內(nèi)部函數(shù)inner為什么采用def inner(*args,**kwargs):的參數(shù)命名形式呢?四、可接受任意數(shù)量參數(shù)的函數(shù)
當(dāng)我們定義不特定數(shù)量參數(shù)的函數(shù)時(shí),可使用*開頭的參數(shù)作可接受任意數(shù)量位置參數(shù)的參數(shù),此時(shí)該參數(shù)作為一個(gè)元組使用。同理,可以使用**開頭的關(guān)鍵字參數(shù)接受任意數(shù)量的關(guān)鍵詞參數(shù),此時(shí)該參數(shù)作為一個(gè)字典使用。如果同時(shí)接受任意數(shù)量的位置參數(shù)和關(guān)鍵字參數(shù),那么只要聯(lián)合使用 * 和 ** 就可以。而 def inner(*args,**kwargs): 是約定俗成的固定寫法。來看個(gè)例子就可以理解這種寫法了。def star(*args,**kwargs):
print(args,kwargs)
star(1,2,3)
star(4,5,name='zhang')
star(7,name='lisi',gender='m')
輸出結(jié)果:
(1, 2, 3) {}
(4, 5) {'name': 'zhang'}
(7,) {'name': 'lisi', 'gender': 'm'}
args搜集所有位置參數(shù),kwargs搜集所有關(guān)鍵字參數(shù)。這個(gè)技術(shù)應(yīng)用在inner函數(shù)上,恰如其分:當(dāng)我們調(diào)用@語法時(shí),只有被裝飾函數(shù)sleeps作為func參數(shù)傳入timethis裝飾器中,sleeps的參數(shù)并沒有傳入裝飾器函數(shù)中。裝飾器不知道sleeps函數(shù)的參數(shù)數(shù)量和具體值,若在其中func調(diào)用參數(shù),則相當(dāng)于調(diào)用不特定名稱和數(shù)量的參數(shù)。接受任意參數(shù)的inner函數(shù),進(jìn)一步將參數(shù)傳給在其中執(zhí)行的func函數(shù)。func函數(shù)是被裝飾的原函數(shù)sleeps,傳給inner函數(shù)的*args,**kwargs參數(shù),直接傳遞給了被裝飾函數(shù)func。這樣就實(shí)現(xiàn)了func(*args,**kwargs)相當(dāng)于sleeps(3)的效果。在完成調(diào)用原函數(shù)的基礎(chǔ)上,如何添加計(jì)時(shí)功能的呢?五、增強(qiáng)被裝飾函數(shù)的行為
以下語句實(shí)現(xiàn)了統(tǒng)計(jì)函數(shù)執(zhí)行時(shí)間的功能 ,當(dāng)然也可以實(shí)現(xiàn)比如日志記錄,建立撤銷環(huán)境之類的功能,大同小異。print('start timer:')
start = time.time()
result = func(*args,**kwargs)
end = time.time()
print('end timer:%fs.'%(end - start))
很簡(jiǎn)單,就是在調(diào)用原函數(shù)的語句result = func(*args,**kwargs)前后,包裹上相應(yīng)的計(jì)時(shí)功能。此處func參數(shù)得以在inner內(nèi)部訪問到,還牽涉到一個(gè)不太好理解的話題——閉包,而理解閉包需要先弄清python中變量的作用域規(guī)則。六、變量作用域
Python變量分全局變量,局部變量。另外函數(shù)的參數(shù)是函數(shù)的局部變量。編寫如下代碼:b=3
def func(a):
print(a)
print(b)
b=2
func(2)
讓我們猜猜運(yùn)行結(jié)果,應(yīng)該是1,3對(duì)吧,但執(zhí)行卻提示出錯(cuò):
File "dec.py", line 47, in func
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
提示先用但未賦值。但b是全局變量,一般理解不論是print(b)對(duì)全局變量的讀取,還是b=2對(duì)全局變量的賦值,都不會(huì)出現(xiàn)這個(gè)問題。問題出在b=2語句上,Python對(duì)在函數(shù)定義體中 賦值的變量都認(rèn)為是局部變量。從而導(dǎo)致局部變量b未賦值先使用的問題。為解決這個(gè)問題,若在函數(shù)中重新賦值了全局變量,需要在函數(shù)中使用global聲明其為全局變量。即函數(shù)若讀取全局變量,可以直接使用。但若在函數(shù)體中重新賦值全局變量,那就需要global聲明變量是全局變量。新問題出現(xiàn)了:在裝飾器timethis中,func是其參數(shù),也就是局部變量,這是無疑的。那么在inner函數(shù)中是怎么訪問func的呢?這就牽涉到閉包問題了。七、閉包
閉包指延伸作用域的函數(shù),函訪可訪問定義體之外定義的非全局變量。在例子中,timethis的參數(shù)func就未定義在inner函數(shù)中,而且也不是全局變量。是閉包將其延伸到了inner函數(shù)中,作為自由變量來使用。所以閉包是一種函數(shù),保留了它在定義時(shí)存在的自由變量。本例中,閉包從timethis定義行到return inner這個(gè)范圍,此時(shí)的局部變量func對(duì)于閉包中的inner函數(shù)來說,就是自由變量,可以讀取和使用。但不可在其中對(duì)自由變量賦值。類似于全局變量,當(dāng)我們?cè)谇短椎暮瘮?shù)中對(duì)自由變量訪問時(shí),可以自由使用。但是當(dāng)我們重新對(duì)其賦值時(shí),解釋器會(huì)把這個(gè)值視為一個(gè)局部變量。若需賦值全局變量,需引入global聲明全局變量;若需賦值自由變量,需引入nonlocal聲明自由變量。因此,func是作為自由變量被閉包函數(shù)inner使用的。那么以上語句之后為什么有兩個(gè)返回語句呢?八、返回值和返回函數(shù)
- 第一個(gè)返回值,返回的是func的執(zhí)行結(jié)果,它屬于inner函數(shù)的返回值,等效于sleeps函數(shù)的返回值,這是sleep函數(shù)的應(yīng)有之意。保持了原函數(shù)sleeps對(duì)外結(jié)構(gòu)的一致性。
- 第二個(gè)返回值是inner函數(shù)本身,也就是第三部分講述的函數(shù)作為返回結(jié)果的用法。依據(jù)slepps=timethi(sleeps)語法,其返回結(jié)果是inner函數(shù),傳遞給sleeps函數(shù),使sleeps函數(shù)實(shí)際上等同于inner函數(shù)。所以調(diào)用sleeps(3)相當(dāng)于調(diào)用inner(3)。再加上圍繞他的計(jì)時(shí)功能,故而無損增加了計(jì)時(shí)功能。
綜上,理解裝飾器最重要的是將@ 語句和賦值語句等同起來。同時(shí)需要理解被裝飾函數(shù)作為參數(shù)傳入裝飾器,嵌套函數(shù)對(duì)其進(jìn)行改造,最后作為函數(shù)返回,使被裝飾函數(shù)實(shí)質(zhì)上關(guān)聯(lián)到新函數(shù)上。