聊一聊Python裝飾器的代碼執(zhí)行順序

為什么寫這篇文章?


起因是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))

裝飾器是不是很好玩呢秘通?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末为严,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肺稀,更是在濱河造成了極大的恐慌第股,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件话原,死亡現(xiàn)場離奇詭異夕吻,居然都是意外死亡,警方通過查閱死者的電腦和手機稿静,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門梭冠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人改备,你說我怎么就攤上這事控漠。” “怎么了悬钳?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵盐捷,是天一觀的道長。 經常有香客問我默勾,道長碉渡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任母剥,我火速辦了婚禮架诞,結果婚禮上感局,老公的妹妹穿的比我還像新娘鼻弧。我一直安慰自己呜投,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布炫隶。 她就那樣靜靜地躺著淋叶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伪阶。 梳的紋絲不亂的頭發(fā)上煞檩,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音栅贴,去河邊找鬼斟湃。 笑死,一個胖子當著我的面吹牛檐薯,可吹牛的內容都是我干的桐早。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哄酝!你這毒婦竟也來了?” 一聲冷哼從身側響起祷膳,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤陶衅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后直晨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搀军,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年勇皇,在試婚紗的時候發(fā)現(xiàn)自己被綠了罩句。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡敛摘,死狀恐怖门烂,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情兄淫,我是刑警寧澤屯远,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站捕虽,受9級特大地震影響慨丐,放射性物質發(fā)生泄漏。R本人自食惡果不足惜泄私,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一房揭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晌端,春花似錦捅暴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惧盹,卻和暖如春乳幸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钧椰。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工粹断, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫡霞。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓瓶埋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子养筒,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345