Python裝飾器的通俗理解

在學(xué)習(xí)Python的過程中人乓,我相信有很多人和我一樣勤篮,對(duì)Python的裝飾器一直覺得很困惑,我也是困惑了好久色罚,并通過思考和查閱才能略有領(lǐng)悟碰缔,我希望以下的內(nèi)容會(huì)對(duì)你有幫助,我也努力通過通俗的方式使得對(duì)Python裝飾器的理解更加的透徹戳护。在文中如有遺漏和不足金抡,歡迎交流和指點(diǎn)。
允許轉(zhuǎn)載并注明出處:http://blog.csdn.net/u013471155

很多人對(duì)裝飾器難以理解腌且,原因是由于以下三點(diǎn)內(nèi)容沒有搞清楚:

  1. 關(guān)于函數(shù)“變量”(或“變量”函數(shù))的理解
  2. 關(guān)于高階函數(shù)的理解
  3. 關(guān)于嵌套函數(shù)的理解

那么如果能對(duì)以上的問題一一攻破梗肝,同時(shí)遵循裝飾器的基本原則,相信會(huì)對(duì)裝飾器有個(gè)很好的理解的铺董。那么我們先來看以下裝飾器的目的及其原則巫击。

1、裝飾器

裝飾器實(shí)際上就是為了給某程序增添功能,但該程序已經(jīng)上線或已經(jīng)被使用坝锰,那么就不能大批量的修改源代碼粹懒,這樣是不科學(xué)的也是不現(xiàn)實(shí)的,因?yàn)榫彤a(chǎn)生了裝飾器顷级,使得其滿足:

  1. 不能修改被裝飾的函數(shù)的源代碼
  2. 不能修改被裝飾的函數(shù)的調(diào)用方式
  3. 滿足1凫乖、2的情況下給程序增添功能

那么根據(jù)需求,同時(shí)滿足了這三點(diǎn)原則弓颈,這才是我們的目的帽芽。因?yàn)椋旅嫖覀儚慕鉀Q這三點(diǎn)原則入手來理解裝飾器恨豁。

等等嚣镜,我要在需求之前先說裝飾器的原則組成:

< 函數(shù)+實(shí)參高階函數(shù)+返回值高階函數(shù)+嵌套函數(shù)+語(yǔ)法糖 = 裝飾器 >

這個(gè)式子是貫穿裝飾器的靈魂所在爬迟!

2橘蜜、需求的實(shí)現(xiàn)

假設(shè)有代碼:

improt time
def test():
    time.sleep(2)
    print("test is running!")
test()

很顯然,這段代碼運(yùn)行的結(jié)果一定是:等待約2秒后付呕,輸出

test is running

  • 那么要求在滿足三原則的基礎(chǔ)上计福,給程序添加統(tǒng)計(jì)運(yùn)行時(shí)間(2 second)功能

在行動(dòng)之前,我們先來看一下文章開頭提到的原因1(關(guān)于函數(shù)“變量”(或“變量”函數(shù))的理解)

2.1徽职、函數(shù)“變量”(或“變量”函數(shù))

假設(shè)有代碼:

x = 1
y = x
def test1():
    print("Do something")
test2 = lambda x:x*2

那么在內(nèi)存中象颖,應(yīng)該是這樣的:

image

很顯然,函數(shù)和變量是一樣的姆钉,都是“一個(gè)名字對(duì)應(yīng)內(nèi)存地址中的一些內(nèi)容
那么根據(jù)這樣的原則说订,我們就可以理解兩個(gè)事情:

  1. test1表示的是函數(shù)的內(nèi)存地址
  2. test1()就是調(diào)用對(duì)在test1這個(gè)地址的內(nèi)容,即函數(shù)

如果這兩個(gè)問題可以理解潮瓶,那么我們就可以進(jìn)入到下一個(gè)原因(關(guān)于高階函數(shù)的理解)

2.2高階函數(shù)

那么對(duì)于高階函數(shù)的形式可以有兩種:

  1. 把一個(gè)函數(shù)名當(dāng)作實(shí)參傳給另外一個(gè)函數(shù)(“實(shí)參高階函數(shù)”)
  2. 返回值中包含函數(shù)名(“返回值高階函數(shù)”)

那么這里面所說的函數(shù)名陶冷,實(shí)際上就是函數(shù)的地址,也可以認(rèn)為是函數(shù)的一個(gè)標(biāo)簽而已毯辅,并不是調(diào)用埂伦,是個(gè)名詞。如果可以把函數(shù)名當(dāng)做實(shí)參思恐,那么也就是說可以把函數(shù)傳遞到另一個(gè)函數(shù)沾谜,然后在另一個(gè)函數(shù)里面做一些操作,根據(jù)這些分析來看胀莹,這豈不是滿足了裝飾器三原則中的第一條基跑,即不修改源代碼而增加功能。那我們看來一下具體的做法:

還是針對(duì)上面那段代碼:

improt time

def test():
    time.sleep(2)
    print("test is running!")

def deco(func):  
    start = time.time()
    func() #2
    stop = time.time()
    print(stop-start)

deco(test) #1

我們來看一下這段代碼描焰,在#1處媳否,我們把test當(dāng)作實(shí)參傳遞給形參func,即func=test。注意逆日,這里傳遞的是地址嵌巷,也就是此時(shí)func也指向了之前test所定義的那個(gè)函數(shù)體,可以說在deco()內(nèi)部室抽,func就是test搪哪。在#2處,把函數(shù)名后面加上括號(hào)坪圾,就是對(duì)函數(shù)的調(diào)用(執(zhí)行它)晓折。因此,這段代碼運(yùn)行結(jié)果是:

test is running!
the run time is 3.0009405612945557

我們看到似乎是達(dá)到了需求兽泄,即執(zhí)行了源程序漓概,同時(shí)也附加了計(jì)時(shí)功能,但是這只滿足了原則1(不能修改被裝飾的函數(shù)的源代碼)病梢,但這修改了調(diào)用方式胃珍。假設(shè)不修改調(diào)用方式,那么在這樣的程序中蜓陌,被裝飾函數(shù)就無法傳遞到另一個(gè)裝飾函數(shù)中去觅彰。

那么再思考,如果不修改調(diào)用方式钮热,就是一定要有test()這條語(yǔ)句填抬,那么就用到了第二種高階函數(shù),即返回值中包含函數(shù)名

如下代碼:

improt time

def test():
    time.sleep(2)
    print("test is running!")

def deco(func):  
    print(func)
    return func 
t = deco(test) #3
#t()#4

test()

我們看這段代碼隧期,在#3處飒责,將test傳入deco(),在deco()里面操作之后仆潮,最后返回了func宏蛉,并賦值給t。因此這里test => func => t鸵闪,都是一樣的函數(shù)體檐晕。最后在#4處保留了原來的函數(shù)調(diào)用方式。
看到這里顯然會(huì)有些困惑蚌讼,我們的需求不是要計(jì)算函數(shù)的運(yùn)行時(shí)間么辟灰,怎么改成輸出函數(shù)地址了。是因?yàn)榇凼瑔为?dú)采用第二張高階函數(shù)(返回值中包含函數(shù)名)的方式芥喇,并且保留原函數(shù)調(diào)用方式,是無法計(jì)時(shí)的凰萨。如果在deco()里計(jì)時(shí)继控,顯然會(huì)執(zhí)行一次械馆,而外面已經(jīng)調(diào)用了test(),會(huì)重復(fù)執(zhí)行武通。這里只是為了說明第二種高階函數(shù)的思想霹崎,下面才真的進(jìn)入重頭戲。

2.3 嵌套函數(shù)

嵌套函數(shù)指的是在函數(shù)內(nèi)部定義一個(gè)函數(shù)冶忱,而不是調(diào)用尾菇,如:

def func1():
    def func2():
        pass
而不是
def func1():
    func2()

另外還有一個(gè)題外話,函數(shù)只能調(diào)用和它同級(jí)別以及上級(jí)的變量或函數(shù)囚枪。也就是說:里面的能調(diào)用和它縮進(jìn)一樣的和他外部的派诬,而內(nèi)部的是無法調(diào)用的。

那么我們?cè)倩氐轿覀冎暗哪莻€(gè)需求链沼,想要統(tǒng)計(jì)程序運(yùn)行時(shí)間默赂,并且滿足三原則。

代碼:

improt time

def timer(func) #5
    def deco():  
        start = time.time()
        func()
        stop = time.time()
        print(stop-start)
    return deco

test = timer(test) #6

def test():
    time.sleep(2)
    print("test is running!")   
test() #7

這段代碼可能會(huì)有些困惑括勺,怎么忽然多了這么多缆八,暫且先接受它,分析一下再來說為什么是這樣朝刊。

首先耀里,在#6處蜈缤,把test作為參數(shù)傳遞給了timer()拾氓,此時(shí),在timer()內(nèi)部底哥,func = test咙鞍,接下來,定義了一個(gè)deco()函數(shù)趾徽,當(dāng)并未調(diào)用续滋,只是在內(nèi)存中保存了,并且標(biāo)簽為deco孵奶。在timer()函數(shù)的最后返回deco()的地址deco疲酌。

然后再把deco賦值給了test始锚,那么此時(shí)test已經(jīng)不是原來的test了蜓肆,也就是test原來的那些函數(shù)體的標(biāo)簽換掉了,換成了deco蔚叨。那么在#7處調(diào)用的實(shí)際上是deco()载绿。

那么這段代碼在本質(zhì)上是修改了調(diào)用函數(shù)粥诫,但在表面上并未修改調(diào)用方式,而且實(shí)現(xiàn)了附加功能崭庸。

那么通俗一點(diǎn)的理解就是:

把函數(shù)看成是盒子怀浆,test是小盒子谊囚,deco是中盒子timer是大盒子执赡。程序中镰踏,把小盒子test傳遞到大盒子temer中的中盒子deco,然后再把中盒子deco拿出來沙合,打開看看(調(diào)用)

這樣做的原因是:

我們要保留test()余境,還要統(tǒng)計(jì)時(shí)間,而test()只能調(diào)用一次(調(diào)用兩次運(yùn)行結(jié)果會(huì)改變灌诅,不滿足)芳来,再根據(jù)函數(shù)即“變量”,那么就可以通過函數(shù)的方式來回閉包猜拾。于是乎即舌,就想到了,把test傳遞到某個(gè)函數(shù)挎袜,而這個(gè)函數(shù)內(nèi)恰巧內(nèi)嵌了一個(gè)內(nèi)函數(shù)顽聂,再根據(jù)內(nèi)嵌函數(shù)的作用域(可以訪問同級(jí)及以上,內(nèi)嵌函數(shù)可以訪問外部參數(shù))盯仪,把test包在這個(gè)內(nèi)函數(shù)當(dāng)中紊搪,一起返回,最后調(diào)用這個(gè)返回的函數(shù)全景。而test傳遞進(jìn)入之后耀石,再被包裹出來,顯然test函數(shù)沒有弄丟(在包裹里)爸黄,那么外面剩下的這個(gè)test標(biāo)簽正好可以替代這個(gè)包裹(內(nèi)含test())滞伟。

image

至此,一切皆合炕贵,大功告成梆奈,單只差一步。

3称开、 真正的裝飾器

根據(jù)以上分析亩钟,裝飾器在裝飾時(shí),需要在每個(gè)函數(shù)前面加上:

test = timer(test)

顯然有些麻煩鳖轰,Python提供了一種語(yǔ)法糖清酥,即:

@timer

這兩句是等價(jià)的,只要在函數(shù)前加上這句脆霎,就可以實(shí)現(xiàn)裝飾作用总处。

以上為無參形式

4、裝飾有參函數(shù)

improt time

def timer(func)
    def deco():  
        start = time.time()
        func()
        stop = time.time()
        print(stop-start)
    return deco

@timer
def test(parameter): #8
    time.sleep(2)
    print("test is running!")   
test() 

對(duì)于一個(gè)實(shí)際問題睛蛛,往往是有參數(shù)的鹦马,如果要在#8處胧谈,給被修飾函數(shù)加上參數(shù),顯然這段程序會(huì)報(bào)錯(cuò)的荸频。錯(cuò)誤原因是test()在調(diào)用的時(shí)候缺少了一個(gè)位置參數(shù)的菱肖。而我們知道test = func = deco,因此test()=func()=deco()
,那么當(dāng)test(parameter)有參數(shù)時(shí)旭从,就必須給func()和deco()也加上參數(shù)稳强,為了使程序更加有擴(kuò)展性,因此在裝飾器中的deco()和func()和悦,加如了可變參數(shù)*agrs和 **kwargs退疫。

完整代碼如下:

improt time

def timer(func)
    def deco(*args, **kwargs):  
        start = time.time()
        func(*args, **kwargs)
        stop = time.time()
        print(stop-start)
    return deco

@timer
def test(parameter): #8
    time.sleep(2)
    print("test is running!")   
test() 

那么我們?cè)倏紤]個(gè)問題,如果原函數(shù)test()的結(jié)果有返回值呢鸽素?比如:

def test(parameter): 
    time.sleep(2)
    print("test is running!")   
    return "Returned value"

那么面對(duì)這樣的函數(shù)褒繁,如果用上面的代碼來裝飾,最后一行的test()實(shí)際上調(diào)用的是deco()馍忽。有人可能會(huì)問棒坏,func()不就是test()么,怎么沒返回值呢遭笋?

其實(shí)是有返回值的坝冕,但是返回值返回到deco()的內(nèi)部,而不是test()即deco()的返回值瓦呼,那么就需要再返回func()的值喂窟,因此就是:

def timer(func)
    def deco(*args, **kwargs):  
        start = time.time()
        res = func(*args, **kwargs)#9
        stop = time.time()
        print(stop-start)
        return res#10
    return deco

其中,#9的值在#10處返回吵血。

完整程序?yàn)椋?/p>

improt time

def timer(func)
    def deco(*args, **kwargs):  
        start = time.time()
        res = func(*args, **kwargs)
        stop = time.time()
        print(stop-start)
        return res 
    return deco

@timer
def test(parameter): #8
    time.sleep(2)
    print("test is running!")   
    return "Returned value"
test() 

5谎替、帶參數(shù)的裝飾器

又增加了一個(gè)需求,一個(gè)裝飾器蹋辅,對(duì)不同的函數(shù)有不同的裝飾。那么就需要知道對(duì)哪個(gè)函數(shù)采取哪種裝飾挫掏。因此侦另,就需要裝飾器帶一個(gè)參數(shù)來標(biāo)記一下。例如:

@decorator(parameter = value)

比如有兩個(gè)函數(shù):

def task1():
    time.sleep(2)
    print("in the task1")

def task2():
    time.sleep(2)
    print("in the task2")

task1()
task2()

要對(duì)這兩個(gè)函數(shù)分別統(tǒng)計(jì)運(yùn)行時(shí)間尉共,但是要求統(tǒng)計(jì)之后輸出:

the task1/task2 run time is : 2.00……

于是就要構(gòu)造一個(gè)裝飾器timer褒傅,并且需要告訴裝飾器哪個(gè)是task1,哪個(gè)是task2袄友,也就是要這樣:

@timer(parameter='task1') #
def task1():
    time.sleep(2)
    print("in the task1")

@timer(parameter='task2') #
def task2():
    time.sleep(2)
    print("in the task2")

task1()
task2()

那么方法有了殿托,但是我們需要考慮如何把這個(gè)parameter參數(shù)傳遞到裝飾器中,我們以往的裝飾器剧蚣,都是傳遞函數(shù)名字進(jìn)去支竹,而這次旋廷,多了一個(gè)參數(shù),要怎么做呢礼搁?
于是饶碘,就想到再加一層函數(shù)來接受參數(shù),根據(jù)嵌套函數(shù)的概念馒吴,要想執(zhí)行內(nèi)函數(shù)扎运,就要先執(zhí)行外函數(shù),才能調(diào)用到內(nèi)函數(shù)饮戳,那么就有:

def timer(parameter): #
    print("in the auth :", parameter)

    def outer_deco(func): #
        print("in the outer_wrapper:", parameter)

        def deco(*args, **kwargs):

        return deco

    return outer_deco

首先timer(parameter)豪治,接收參數(shù)parameter=’task1/2’,而@timer(parameter)也恰巧帶了括號(hào)扯罐,那么就會(huì)執(zhí)行這個(gè)函數(shù)鬼吵, 那么就是相當(dāng)于:

timer = timer(parameter)
task1 = timer(task1)

后面的運(yùn)行就和一般的裝飾器一樣了:

import time

def timer(parameter):

    def outer_wrapper(func):

        def wrapper(*args, **kwargs):
            if parameter == 'task1':
                start = time.time()
                func(*args, **kwargs)
                stop = time.time()
                print("the task1 run time is :", stop - start)
            elif parameter == 'task2':
                start = time.time()
                func(*args, **kwargs)
                stop = time.time()
                print("the task2 run time is :", stop - start)

        return wrapper

    return outer_wrapper

@timer(parameter='task1')
def task1():
    time.sleep(2)
    print("in the task1")

@timer(parameter='task2')
def task2():
    time.sleep(2)
    print("in the task2")

task1()
task2()

至此,裝飾器的全部?jī)?nèi)容結(jié)束篮赢。
參考:https://blog.csdn.net/u013471155/article/details/68960244

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末齿椅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子启泣,更是在濱河造成了極大的恐慌涣脚,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寥茫,死亡現(xiàn)場(chǎng)離奇詭異遣蚀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)纱耻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門芭梯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弄喘,你說我怎么就攤上這事玖喘。” “怎么了蘑志?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵累奈,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我急但,道長(zhǎng)澎媒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任波桩,我火速辦了婚禮戒努,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镐躲。我一直安慰自己储玫,他們只是感情好侍筛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缘缚,像睡著了一般勾笆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上桥滨,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天窝爪,我揣著相機(jī)與錄音,去河邊找鬼齐媒。 笑死蒲每,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喻括。 我是一名探鬼主播邀杏,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼唬血!你這毒婦竟也來了望蜡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤拷恨,失蹤者是張志新(化名)和其女友劉穎脖律,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腕侄,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡小泉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冕杠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片微姊。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖分预,靈堂內(nèi)的尸體忽然破棺而出兢交,到底是詐尸還是另有隱情,我是刑警寧澤噪舀,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布魁淳,位于F島的核電站,受9級(jí)特大地震影響与倡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昆稿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一纺座、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧溉潭,春花似錦净响、人聲如沸少欺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赞别。三九已至,卻和暖如春配乓,著一層夾襖步出監(jiān)牢的瞬間仿滔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工犹芹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留崎页,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓腰埂,卻偏偏與公主長(zhǎng)得像飒焦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屿笼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載來自:http://blog.csdn.net/u013471155 在學(xué)習(xí)Python的過程中牺荠,我相信有很多...
    JM68閱讀 571評(píng)論 3 9
  • 包(lib)、模塊(module) 在Python中驴一,存在包和模塊兩個(gè)常見概念休雌。 模塊:編寫Python代碼的py...
    清清子衿木子水心閱讀 3,800評(píng)論 0 27
  • 〇、前言 本文共108張圖蛔趴,流量黨請(qǐng)慎重挑辆! 歷時(shí)1個(gè)半月,我把自己學(xué)習(xí)Python基礎(chǔ)知識(shí)的框架詳細(xì)梳理了一遍孝情。 ...
    Raxxie閱讀 18,918評(píng)論 17 410
  • 婚禮日期:農(nóng)歷12.26 (主持人:阿方18711886909) 主持人暖場(chǎng)箫荡、致開場(chǎng)白 新郎持手捧花上臺(tái) 新娘在父...
    易阿方閱讀 178評(píng)論 0 0
  • 晉幽公九年(前425年)魁亦,趙襄子趙無恤去世,魏斯繼任為晉國(guó)正卿羔挡,任用上郡守李悝實(shí)行變法洁奈。晉烈公十一年(前405年)...
    殺菠蘿閱讀 701評(píng)論 1 0