為什么寫這篇文章?
起因是QQ群里邊有人提了一個問題:之前導入模塊只需要1~2秒越锈,為什么現(xiàn)在變成需要2~3分鐘宠进?
我的第一感覺是:是不是導入的模塊頂層代碼里邊,做了什么耗時的事情绘梦。隔了一天橘忱,他的問題解決了,下邊是按照他的代碼寫了一個類似的例子:
import time
def set_log(func):
def wrap(*args, **kwargs):
return func(*args, **kwargs)
time.sleep(4)
return wrap
@set_log
def demo():
pass
為什么導入這個模塊的時候谚咬,會運行time.sleep(4)
鹦付,明明沒有調用demo函數(shù)呀?這就要從Python裝飾器代碼的執(zhí)行順序說起了择卦。
簡單介紹下裝飾器
在正式開始之前敲长,先簡單科普一下Python的裝飾器,裝飾器可以對已有的函數(shù)秉继,添加額外的功能祈噪,甚至于完全改變函數(shù)的執(zhí)行效果。舉個例子尚辑,現(xiàn)在想統(tǒng)計幾個函數(shù)的執(zhí)行耗時辑鲤,函數(shù)是這樣的:
import time
import random
def a_func():
time.sleep(random.randint(1, 5))
當然,我們可以這么寫
def a_func():
start_time = time.time()
time.sleep(random.randint(1, 5))
print("cost time: {}".format(time.time() - start_time))
這樣帶來的問題是代碼的可維護性不佳杠茬,尤其你有多個函數(shù)需要計算耗時的時候月褥,萬一某天突然想去掉這些統(tǒng)計代碼呢~
所以像這種有切面需求的場景,裝飾器是一個非常漂亮的設計瓢喉。
def cost_time(func):
def wrap(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
print("cost time: {}".format(time.time() - start_time))
return result
return wrap
@cost_time
def a_func():
time.sleep(random.randint(1, 5))
只需要對統(tǒng)計耗時的函數(shù)掛上一個裝飾器宁赤,結果就自動出來,無需改動之前的代碼栓票,非常方便决左。
Python也支持帶參數(shù)的裝飾器,比如剛剛的cost_time
加入一個報警機制走贪,如果函數(shù)執(zhí)行耗時大于1秒佛猛,就發(fā)出警告。
def cost_time(warn=1):
def wrap(func):
def _wrap(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
cost = time.time() - start_time
print("cost time: {}".format(cost))
if cost > warn:
print("warning, cost time is {} !!!".format(cost))
return result
return _wrap
return wrap
@cost_time()
def a_func():
time.sleep(random.randint(1, 5))
a_func()
執(zhí)行結果:
cost time: 3.0002505779266357
warning, cost time is 3.0002505779266357 !!!
Python裝飾器代碼的執(zhí)行順序
回到我們的主題坠狡,首先把剛剛的例子加入一些打蛹陶摇:
import time
print("準備編寫裝飾器")
def set_log(func):
print("裝飾器頂層代碼")
def wrap(*args, **kwargs):
print("裝飾器內層代碼")
return func(*args, **kwargs)
# time.sleep(4)
print("準備返回wrap對象")
return wrap
print("準備編寫demo函數(shù)")
@set_log
def demo():
print("正在運行demo函數(shù)")
if __name__ == '__main__':
print("準備運行demo函數(shù)")
demo()
運行結果是:
準備編寫裝飾器
準備編寫demo函數(shù)
裝飾器頂層代碼
準備返回wrap對象
準備運行demo函數(shù)
裝飾器內層代碼
正在運行demo函數(shù)
所以在運行demo函數(shù)之前,已經做了:
- 準備編寫裝飾器
- 準備編寫demo函數(shù)
- 裝飾器頂層代碼
- 準備返回wrap對象
也就是說逃沿,就算你沒有運行demo函數(shù)码荔,只是導入了這個模塊,上邊的這4件事情感挥,都是會一一執(zhí)行的。
是不是有點懵越败?
讓我們從頭開始触幼,梳理一遍這個過程。
Python的代碼是從上往下依次執(zhí)行的究飞,所以當你導入這個模塊置谦,第一句運行的代碼就是
import time
然后就來到了
print("準備編寫裝飾器")
接著是來到了set_log
裝飾器函數(shù)的定義
def set_log(func):
需要注意的時候堂鲤,在這里Python只運行了函數(shù)的定義語句,對于函數(shù)內部的執(zhí)行媒峡,是直接跳過去的瘟栖,并沒有運行。
繼續(xù)往下谅阿,來到了
print("準備編寫demo函數(shù)")
此時重點來了半哟,到了demo函數(shù)的定義了
@set_log
def demo():
print("正在運行demo函數(shù)")
因為代碼從上往下依次運行的機制,Python解釋器首先到了@set_log
這句代碼签餐,@這個符號是Python提供的語法糖寓涨,它本質上是為了簡化了裝飾器的寫法,上邊的寫法等于
def demo():
print("正在運行demo函數(shù)")
demo = set_log(demo)
于是Python開始執(zhí)行set_log
裝飾器氯檐,來完成對demo函數(shù)的修飾戒良。
def set_log(func):
print("裝飾器頂層代碼")
def wrap(*args, **kwargs):
print("裝飾器內層代碼")
return func(*args, **kwargs)
# time.sleep(4)
print("準備返回wrap對象")
return wrap
首先來到的是
print("裝飾器頂層代碼")
然后是裝飾器內部wrap函數(shù)的定義,同樣是冠摄,只運行了定義語句糯崎,跳過函數(shù)的內部執(zhí)行代碼
def wrap(*args, **kwargs):
然后來到了打印“準備返回wrap對象”,以及返回wrap對象河泳,要注意沃呢,在返回了wrap函數(shù)對象后,此時demo函數(shù)乔询,其實已經被替換成了wrap函數(shù)對象樟插。
print("準備返回wrap對象")
return wrap
完成了對demo函數(shù)的修飾后,代碼也來到了最后的調用demo函數(shù)的部分
if __name__ == '__main__':
print("準備運行demo函數(shù)")
demo()
新的重點來了~
上邊說到竿刁,在裝飾器內部返回了wrap對象后黄锤,demo已經被替換成了wrap函數(shù)對象了。
也就說說食拜,運行 demo()
鸵熟,其實就是運行wrap()
def wrap(*args, **kwargs):
print("裝飾器內層代碼")
return func(*args, **kwargs)
所以代碼來到了wrap的函數(shù)內部,首先當然就是打印了“裝飾器內層代碼”负甸。接下來是
return func(*args, **kwargs)
這里的func是不是很眼熟流强?我們回去看看set_log裝飾器的定義:
def set_log(func):
print("裝飾器頂層代碼")
def wrap(*args, **kwargs):
print("裝飾器內層代碼")
return func(*args, **kwargs)
# time.sleep(4)
print("準備返回wrap對象")
return wrap
func就是我們一開始傳給set_log
裝飾器修飾的demo函數(shù),還記得上邊寫的呻待,裝飾器的兩種寫法嗎打月?
@set_log
def demo():
pass
# 等同于:
def demo():
pass
demo = set_log(demo)
于是代碼進入到了demo函數(shù)的內部去了~
def demo():
print("正在運行demo函數(shù)")
執(zhí)行完畢,最終搞定蚕捉,一個裝飾器的代碼執(zhí)行順序就是這么走過來的奏篙。
最后,再來一個多重+帶參數(shù)的裝飾器的復雜一點的例子~
print("準備編寫裝飾器")
def set_log_first(func):
print("set_log_first裝飾器頂層代碼")
def wrap(*args, **kwargs):
print("set_log_first裝飾器內層代碼")
return func(*args, **kwargs)
print("set_log_first準備返回wrap對象")
return wrap
def set_log_second(times=1):
print("set_log_second裝飾器頂層代碼")
def wrap(func):
print("set_log_second裝飾器中間層代碼")
def _wrap(*args, **kwargs):
print("set_log_second裝飾器內層代碼")
return func(*args, **kwargs)
print("set_log_second準備返回中間層的_wrap對象")
return _wrap
print("set_log_second準備返回頂層的wrap對象")
return wrap
print("準備編寫demo函數(shù)")
@set_log_first
@set_log_second()
def demo():
print("正在運行demo函數(shù)")
if __name__ == '__main__':
print("準備運行demo函數(shù)")
demo()
輸出是~
準備編寫裝飾器
準備編寫demo函數(shù)
set_log_second裝飾器頂層代碼
set_log_second準備返回頂層的wrap對象
set_log_second裝飾器中間層代碼
set_log_second準備返回中間層的_wrap對象
set_log_first裝飾器頂層代碼
set_log_first準備返回wrap對象
準備運行demo函數(shù)
set_log_first裝飾器內層代碼
set_log_second裝飾器內層代碼
正在運行demo函數(shù)
這里理解的重點就是,下邊的兩個寫法是等價的
@set_log_first
@set_log_second()
def demo():
print("正在運行demo函數(shù)")
# 等價于
demo = set_log_first(set_log_second()(demo))
裝飾器是不是很好玩呢秘通?