由於裝飾器的結構和使用形式,相信很多python的初學者在學習的過程中有很多困惑,本文儘量站在初學者的角度,用大白話和簡單的代碼對裝飾器進行講解,繞開閉包和對象引用的概念,希望儘可能減少初學者在學習裝飾器時的困惑。
1 什麼是裝飾器
其實對於初學者來說,最大的疑惑可能是裝飾器是幹什麼用的?爲什麼我在編程的過程中基本上用不到,我在什麼場合下必須用它呢?
其實裝飾器很簡單,從名字上就可以看出它的功能,它就是裝飾用的。而它的裝飾對象就是函數,而所謂的“裝飾”就是給函數添加功能的意思。
這個時候你可能會想,給函數添加功能,直接修改函數不就行了,爲啥要用裝飾器呢。事實上確實是這樣,用裝飾器能完成的功能,直接修改函數也能達到同樣的效果,這也是爲什麼初學者基本上用不到裝飾器的原因。但是在實際的項目的某個階段,你可能不允許改動測試好的函數,而想增加一些功能,比如統計這個函數運行的時間,那麼這個時候,裝飾器就派上用場了。這也恰恰是裝飾器的本質所在,在不改變函數和它的引用的情況下,增加函數的功能。
那麼基於以上的分析,假設現在我們有如下的函數,其作用是在兩秒後,輸出打印的內容。在這個函數的基礎上,我們提出以下需求,後文將在解決這個需求的過程中,逐步深入裝飾器的原理。
import time
def print_function():
time.sleep(2)
print("this is a print function")
print_function() #1
1、給該函數增加一個運行耗時統計的功能
2、不改動這個函數的內部代碼
3、不改動這個函數的引用
好,如果你沒有思考過這個問題,可以在這裏停下來想一想再繼續下面的內容。
2 問題的解決
現在我們想要增加一個統計這個函數運行時間的功能,而不改動函數的源碼,首先我們想到的是,在這個函數引用(#1 處)的外部,添加這些計時功能的代碼,就像下面這樣:
import time
def print_function():
time.sleep(2)
print("this is a print function")
time_start=time.time()
print_function() #1
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
可以看到,通過在 #1處的上下增加代碼,我們實現了函數的計數功能,而且沒有改變函數的內部代碼。那麼這些增加的計時所用的代碼就是所謂的“裝飾”,其實裝飾器的原理就是這麼簡單。但是這種方式增加的代碼,讓函數的引用變得更加複雜。現在我們換一種方式,讓代碼看起來更加優雅。很自然的一個想法是,我們把最後四行再封裝成一個函數,去引用它。
import time
def print_function():
time.sleep(2)
print("this is a print function")
def print_function_time():
time_start=time.time()
print_function() #1
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
print_function_time()
很顯然,print_function_time() 就是我們要的函數,但是由於它的函數名改變了,不符合我們一開始提出的問題。那有沒有什麼辦法不改名函數名,也能實現同樣的功能呢,答案就是高階函數。其實不要被高階函數這個名字唬住了,它和普通函數唯一的區別就在於它返回的不是數值、字符串、列表等這樣普通的對象,它返回的是一個函數。我們知道,當我們使用下面這條語句之後,f() 和print_function() 其實是等價的。
f=print_function()
那麼我們自然想到,把我們要“裝飾”的函數(print_function)傳到一個函數裏面,增加計時功能之後,再把它當做返回值返回,廢話不多說,上代碼。
import time
def print_function():
time.sleep(2)
print("this is a print function")
def timer(func): #1
def deco():
time_start=time.time()
func() #2
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return deco
print_function=timer(print_function) #3
print_function()
#1 處我們定義了一個裝飾函數,#2 處爲傳入的被裝飾的函數,最外層的timer函數返回的是 deco函數,也就是“裝飾後”的 func 函數。
因此我們在 #3處 引用 timer(print_function) 其實就是引用了deco(),也就是被裝飾後的print_function 函數,最後一行中print_function()的引用雖然和之前定義的函數名相同,但實際上,他已經在原來的基礎上增加了計時功能。
至此,我們已經實現了開頭提出來的問題,在不改變函數內部代碼和函數引用的情況下,增加計時功能。這就是一個裝飾器的工作原理和過程,其中print_function 我們可以稱之爲被裝飾的對象(函數),外部的 timer 就是裝飾器,當函數被當成參數傳入到裝飾器中,裝飾器返回一個被裝飾之後的函數,這個過程我們可以稱之爲裝飾。
當然python 提供了一種比較直觀的裝飾器語法,來代替上面這種樸素的寫法,使代碼更加優雅。
3 python 中裝飾器的寫法
在我們上面的寫法中,需要在每個被裝飾的函數前面加上一句
print_function=timer(print_function)
python 提供了一種更加簡潔的寫法。在需要裝飾的函數定義(#1 處)上面加 @timer 語句,就表示該函數被timer函數裝飾,注意,在引用裝飾函數(@timer)之前,需要先定義裝飾函數,不然會報錯。下面這段代碼是python 裝飾器的標準寫法,但是它和第2小節中的那段代碼是等效的。
import time
def timer(func):
def deco():
time_start=time.time()
func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return deco
@timer # 1
def print_function():
time.sleep(2)
print("this is a print function")
print_function()
4 裝飾有返回值的函數和帶有參數的函數
相信看到這,你已經對裝飾器的原理和作用有了一個初步的理解。在上面我們被裝飾的函數是一個沒有返回值和沒有參數的函數,那麼假如要裝飾有返回值和有參數的函數,該怎麼做呢,首先,我們來看有返回值的函數。
4.1 裝飾有返回值的函數
裝飾有返回值的函數其實很簡單,在上面一段代碼中,其實我們最後引用的print_function() 其實就是 deco()函數,顯然目前deco 函數是沒有返回值的,所以需要在deco函數中添加一個 return 項,那麼要return 什麼呢?因爲我們要的是被裝飾函數(print_function)的返回值,其實就是要返回其內部 func()的返回值,所以需要把func()的返回值記錄下來,然後在deco 函數中返回:
import time
def timer(func):
def deco():
time_start=time.time()
value=func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return value
return deco
@timer # 1
def print_function():
time.sleep(2)
print("this is a print function")
return "run over"
print_function() #1 輸出 'run over'
4.2 返回帶有參數的函數
實際中的函數往往是有參數的,如果將上面被裝飾的print_function 函數定義處直接改成有參函數,如下面的代碼所示,顯然這段代碼是會報錯的,因爲在裝飾函數中傳入的函數是沒有參數的函數。
import time
def timer(func):
def deco():
time_start=time.time()
value=func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return value
return deco
@timer
def print_function(parameter): #2 增加形參
time.sleep(2)
print("this is a print function")
return "run over"
print_function()
爲了裝飾帶有參數的函數,需要給裝飾器中的deco()和func()也加上參數,考慮到參數數量的不確定性和不同類型(普通參數和關鍵字參數),參數採用可變參數(**args)和可變關鍵字參數(**args)兩者的組合,這樣就可以適應有各種參數的函數。
import time
def timer(func):
def deco(*args,**kwargs):
time_start=time.time()
value=func(*args,**kwargs)
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return value
return deco
@timer
def print_function(parameter): #2 增加形參
time.sleep(2)
print("this is a print function",parameter)
return "run over"
print_function('with parameter') # 輸出 this is a print function with parameter
5 帶有參數的裝飾器
有時候,同一個裝飾器,針對不同的輸入函數要採取不同的裝飾方式。因此就需要有參數來標記對哪個函數採取哪種裝飾措施,這種帶有參數的裝飾器的使用如下:
@timer(parameter=parmeter_value)
比如我們有兩個print_function,分別是print_function1 和 print_function2,如下所示:
def print_function1():
time.sleep(1)
print("this is the print function 1")
def print_function2()
time_sleep(2)
print(this is the print function 2)
針對不同的打印函數,除了都需要計時外,裝飾器還要針對傳入的兩個不同函數輸出“in print_function1”和“in print_function1/print_function2”,這個時候裝飾器就需要有參數來區分傳入的函數爲print_function1還是print_function2,不廢話,上代碼:
import time
def timer(parameter):
def outer(func): #1 增加一層函數
def deco():
print("in",parameter) #1 針對不同的輸出不同的語句
time_start=time.time()
func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return deco
return outer
@timer (parameter="print_function1")
def print_function1():
time.sleep(1)
print("this is the print function1")
@timer(parameter="print_function2")
def print_function2():
time.sleep(2)
print("this is the print function2")
print_function1() # 輸出 in print_function1
print_function2() # 輸出 in print_function2
由於要把paramter 參數傳遞到裝飾器中,我們以往都是將函數名傳入裝飾器,現在多了一個參數,要怎麼解決呢。一個自然的想法就是直接在原來timer(func)中直接增加一個參數,變爲time(func, parameter),但是這樣直接將兩個參數放到一起會引起後面引用的錯誤。
實際的做法就是在 #1 處增加一層函數來接受參數,其產生的效果如下所示:
timer=timer(parameter)
print_function=timer(print_function)
其運行就和一般的裝飾器一樣了。
6、進一步閱讀
其實裝飾器是閉包思想的一個應用,關於閉包的介紹,請參考談談自己的理解:python中閉包,閉包的實質。
參考文章: