裝飾器
此爲個人學習筆記,內容僅供參考,轉載請註明作者,如有錯誤,歡迎指正。)
記得以前學到這裏,看了很多關於python裝飾器的文章,對於裝飾器還是一知半解,後來在一些實例中, 通過最樸實的方法總算是摸清楚了什麼是裝飾器?以下是個人對裝飾器的筆記。
什麼是裝飾器?
在學習裝飾器之前,我們必須搞清楚一點,裝飾器究竟是什麼? 用文字說明, 裝飾器實際上就是爲了給某個函數,或者稱某個程序添加一個功能。並且考慮 到該函數或程序已經被引用,所以我們不能對函數或程序本身進行修改,所以 有了裝飾器,可以想象成,我們人拿上斧頭,擁有了砍樹的能力,這就是裝飾器的作用。
裝飾器需要滿足以下兩點:
- 不修改原程序或函數的代碼
- 不改變函數或程序的調用方法
ps:此處如果不理解,我們還用人砍樹的例子。比如說小明他是個人,你要讓他砍樹,他怎麼砍, 必須要讓他擁有砍樹能力,那麼問題來了,怎麼讓他擁有這個能力呢?好吧可以給他注射藥劑, 讓他變強,但是這就不滿足上述第一點了,被改造後的小明我們就不認識他了,不知道怎麼讓他去砍樹, 所以就必須有前提條件1,不得修改源代碼。 第二條,不改變調用方法。原本把我想讓小明去砍樹就對 小明下命令,“嘿小明去砍個樹”,但是小明不會啊,怎麼辦呢,讓一個會的人帶着小明去,這個過程就成了什麼呢? 你讓某個人帶着小明去,而不是你讓小明去,這就出現了調用方法的改變。
實例中理解
第一步:裝飾器的前奏
定義一個測試的函數,返回輸入的值
def test1(a):
return a
上述定義的函數,它的作用就是返回輸入值,那麼接下來要考慮給它加一個計算它自身運行時間的功能。最簡單的思路是,記錄開始時間->運行函數->記錄結束時間, 用結束時間減去開始時間就是我們想要的結果了,如下:
import time
start_time = time.time() # 開始時間
test1(250) # 要測試的函數
stop_time = time.time() # 結束時間
delta_time = stop_time - start_time # 時間間隔,即運行時間
好了,簡單地實現了。根據需求應該把這個過程封裝在函數內,調用一次就返回test1的運行時間。塞到函數裏面大概是這樣:
def test2()
start_time = time.time() # 開始時間
test1(250) # 要測試的函數
stop_time = time.time() # 結束時間
delta_time = stop_time - start_time # 時間間隔,即運行時間
print('{:f}'.format(delta_time)) # 格式化打印
運行一下
>>> test2(250)
'0.000001'
此處可以看到,將上述過程封裝到 test2 之後,通過調用 test2 就能得到 test1 函數的運行時間, 但是不難發現有兩個點:
(1)改變了調用方法,根據裝飾器的描述,應該直接調用test1就得到運行時間
(2)此處只研究了test1(250)也就是說此時test1只有一個固定參數,這不能保證是否會因爲參數的不同運行時間不同。
針對這兩個點進一步改進
首先來解決一下關於調用的時候不再是固定參數,如下:
def test2(func):
def test3(x):
start_time = time.time() # 開始時間
func(x) # 要測試的函數
stop_time = time.time() # 結束時間
delta_time = stop_time - start_time # 時間間隔,即運行時間
return '{:f}'.format(delta_time) # f返回格式化時間
return test3
def test1(a):
return a
運行一下
>>> test2(test3)(250)
'0.000002'
現在有三個函數test1, test2, test3。整個流程還是得梳理清楚,是爲了計算test1的運行時間, 那麼還是"記錄開始時間->運行函數->記錄結束時間"這一個流程。
在上面這段代碼中,首先把 test1 的函數地址當成參數傳入到了 test2 , 此時 test2(func) 相當於 test2(test1),在 test2 中,再定義一個test3。把上述求運行時間的流程放進去,在test3中可以看到一個func(x), func是我們傳入的地址也就是test1,要計算test1的運行時間,那肯定是需要調用的, func() 就是 func 的調用,func(x) 相當於 test1(x)。
再來看 test2 函數的返回值,是test3的地址,一方面不會因爲調用而報錯,只是拿了 test3 函數的地址; 一方面是爲了取得 test3 以保證後面的展開。現在目的就很明確了。調用test2, 傳入test1的地址,又因爲是返回了test3的地址,實際上經過一頓操作,test2(test1) 就像是 test3 的地址, test2(test1)() 就是在實例化 test3 了, 即 test3(), 顯而易見,其中 x 即最初傳入的 250 。現在可以這樣來獲得各種參數中test1的運行時間。
第二步,實現裝飾器
剛剛說到兩個點,已經解決了一個,再去解決調用方法。在test1函數加一個@test2,這是一個語法糖, 效果如下:
def test2(func):
def test3(x):
start_time = time.time() # 開始時間
func(x) # 要測試的函數
stop_time = time.time() # 結束時間
delta_time = stop_time - start_time # 時間間隔,即運行時間
return '{:f}'.format(delta_time) # f返回格式化時間
return test3
@test2
def test1(a):
return a
運行一下
>>> test1(250)
'0.000001'
加入語法糖之後直接調用test1就得出了想要的結果。那麼這個語法糖的作用就顯而易見了。 就是替代test2(test1)的。
就是說test2(test1)(250) 直接被語法糖省略成了test1(250),這二者是相同的效果,但是滿足了不改變調用方法,而且我們自始至終都沒有修改過test1裏面的源代碼。
這就是裝飾器。(2018.11.16)