Python裝飾器的通俗理解

在學習Python的過程中,我相信有很多人和我一樣,對Python的裝飾器一直覺得很困惑,我也是困惑了好久,并通過思考和查閱才能略有領悟,我希望以下的內容會對你有幫助,我也努力通過通俗的方式使得對Python裝飾器的理解更加的透徹。在文中如有遺漏和不足,歡迎交流和指點。

分享來源:http://blog.csdn.net/u013471155

很多人對裝飾器難以理解,原因是由于以下三點內容沒有搞清楚:

關于函數“變量”(或“變量”函數)的理解

關于高階函數的理解

關于嵌套函數的理解

那么如果能對以上的問題一一攻破,同時遵循裝飾器的基本原則,相信會對裝飾器有個很好的理解的。那么我們先來看以下裝飾器的目的及其原則。

1、裝飾器

裝飾器實際上就是為了給某程序增添功能,但該程序已經上線或已經被使用,那么就不能大批量的修改源代碼,這樣是不科學的也是不現實的,因為就產生了裝飾器,使得其滿足:

不能修改被裝飾的函數的源代碼

不能修改被裝飾的函數的調用方式

滿足1、2的情況下給程序增添功能

那么根據需求,同時滿足了這三點原則,這才是我們的目的。因為,下面我們從解決這三點原則入手來理解裝飾器。

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

< 函數+實參高階函數+返回值高階函數+嵌套函數+語法糖 = 裝飾器 >

這個式子是貫穿裝飾器的靈魂所在!

2、需求的實現

假設有代碼:

improt time

def test():

? ? time.sleep(2)

? ? print("test is running!")

test()

很顯然,這段代碼運行的結果一定是:等待約2秒后,輸出

test is running

那么要求在滿足三原則的基礎上,給程序添加統計運行時間(2?second)功能

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

2.1、函數“變量”(或“變量”函數)

假設有代碼:

x = 1

y = x

def test1():

? ? print("Do something")

test2 = lambda x:x*2

那么在內存中,應該是這樣的:


很顯然,函數和變量是一樣的,都是“一個名字對應內存地址中的一些內容”?

那么根據這樣的原則,我們就可以理解兩個事情:

test1表示的是函數的內存地址

test1()就是調用對在test1這個地址的內容,即函數

如果這兩個問題可以理解,那么我們就可以進入到下一個原因(關于高階函數的理解)

2.2高階函數

那么對于高階函數的形式可以有兩種:

把一個函數名當作實參傳給另外一個函數(“實參高階函數”)

返回值中包含函數名(“返回值高階函數”)

那么這里面所說的函數名,實際上就是函數的地址,也可以認為是函數的一個標簽而已,并不是調用,是個名詞。如果可以把函數名當做實參,那么也就是說可以把函數傳遞到另一個函數,然后在另一個函數里面做一些操作,根據這些分析來看,這豈不是滿足了裝飾器三原則中的第一條,即不修改源代碼而增加功能。那我們看來一下具體的做法:

還是針對上面那段代碼:

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當作實參傳遞給形參func,即func=test。注意,這里傳遞的是地址,也就是此時func也指向了之前test所定義的那個函數體,可以說在deco()內部,func就是test。在#2處,把函數名后面加上括號,就是對函數的調用(執行它)。因此,這段代碼運行結果是:

test is running!?

the run time is 3.0009405612945557

我們看到似乎是達到了需求,即執行了源程序,同時也附加了計時功能,但是這只滿足了原則1(不能修改被裝飾的函數的源代碼),但這修改了調用方式。假設不修改調用方式,那么在這樣的程序中,被裝飾函數就無法傳遞到另一個裝飾函數中去。

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

如下代碼:

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,都是一樣的函數體。最后在#4處保留了原來的函數調用方式。?

看到這里顯然會有些困惑,我們的需求不是要計算函數的運行時間么,怎么改成輸出函數地址了。是因為,單獨采用第二張高階函數(返回值中包含函數名)的方式,并且保留原函數調用方式,是無法計時的。如果在deco()里計時,顯然會執行一次,而外面已經調用了test(),會重復執行。這里只是為了說明第二種高階函數的思想,下面才真的進入重頭戲。

2.3 嵌套函數

嵌套函數指的是在函數內部定義一個函數,而不是調用,如:

def func1():

? ? def func2():

? ? ? ? pass

# 而不是

def func1():

? ? func2()

另外還有一個題外話,函數只能調用和它同級別以及上級的變量或函數。也就是說:里面的能調用和它縮進一樣的和他外部的,而內部的是無法調用的。

那么我們再回到我們之前的那個需求,想要統計程序運行時間,并且滿足三原則。

代碼:

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

這段代碼可能會有些困惑,怎么忽然多了這么多,暫且先接受它,分析一下再來說為什么是這樣。

首先,在#6處,把test作為參數傳遞給了timer(),此時,在timer()內部,func = test,接下來,定義了一個deco()函數,當并未調用,只是在內存中保存了,并且標簽為deco。在timer()函數的最后返回deco()的地址deco。

然后再把deco賦值給了test,那么此時test已經不是原來的test了,也就是test原來的那些函數體的標簽換掉了,換成了deco。那么在#7處調用的實際上是deco()。

那么這段代碼在本質上是修改了調用函數,但在表面上并未修改調用方式,而且實現了附加功能。

那么通俗一點的理解就是:

把函數看成是盒子,test是小盒子,deco是中盒子,timer是大盒子。程序中,把小盒子test傳遞到大盒子temer中的中盒子deco,然后再把中盒子deco拿出來,打開看看(調用)

這樣做的原因是:

我們要保留test(),還要統計時間,而test()只能調用一次(調用兩次運行結果會改變,不滿足),再根據函數即“變量”,那么就可以通過函數的方式來回閉包。于是乎,就想到了,把test傳遞到某個函數,而這個函數內恰巧內嵌了一個內函數,再根據內嵌函數的作用域(可以訪問同級及以上,內嵌函數可以訪問外部參數),把test包在這個內函數當中,一起返回,最后調用這個返回的函數。而test傳遞進入之后,再被包裹出來,顯然test函數沒有弄丟(在包裹里),那么外面剩下的這個test標簽正好可以替代這個包裹(內含test())。


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

3、 真正的裝飾器

根據以上分析,裝飾器在裝飾時,需要在每個函數前面加上:

test = timer(test)

顯然有些麻煩,Python提供了一種語法糖,即:

@timer

這兩句是等價的,只要在函數前加上這句,就可以實現裝飾作用。

以上為無參形式

4、裝飾有參函數

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()

對于一個實際問題,往往是有參數的,如果要在#8處,給被修飾函數加上參數,顯然這段程序會報錯的。錯誤原因是test()在調用的時候缺少了一個位置參數的。而我們知道test = func = deco,因此test()=func()=deco()?

,那么當test(parameter)有參數時,就必須給func()和deco()也加上參數,為了使程序更加有擴展性,因此在裝飾器中的deco()和func(),加如了可變參數*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()

那么我們再考慮個問題,如果原函數test()的結果有返回值呢?比如:

def test(parameter):

? ? time.sleep(2)

? ? print("test is running!")?

? ? return "Returned value"

那么面對這樣的函數,如果用上面的代碼來裝飾,最后一行的test()實際上調用的是deco()。有人可能會問,func()不就是test()么,怎么沒返回值呢?

其實是有返回值的,但是返回值返回到deco()的內部,而不是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處返回。

完整程序為:

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、帶參數的裝飾器

又增加了一個需求,一個裝飾器,對不同的函數有不同的裝飾。那么就需要知道對哪個函數采取哪種裝飾。因此,就需要裝飾器帶一個參數來標記一下。例如:

@decorator(parameter = value)

比如有兩個函數:

def task1():

? ? time.sleep(2)

? ? print("in the task1")

def task2():

? ? time.sleep(2)

? ? print("in the task2")

task1()

task2()

要對這兩個函數分別統計運行時間,但是要求統計之后輸出:

the task1/task2 run time is : 2.00……

于是就要構造一個裝飾器timer,并且需要告訴裝飾器哪個是task1,哪個是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()

那么方法有了,但是我們需要考慮如何把這個parameter參數傳遞到裝飾器中,我們以往的裝飾器,都是傳遞函數名字進去,而這次,多了一個參數,要怎么做呢??

于是,就想到再加一層函數來接受參數,根據嵌套函數的概念,要想執行內函數,就要先執行外函數,才能調用到內函數,那么就有:

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),接收參數parameter=’task1/2’,而@timer(parameter)也恰巧帶了括號,那么就會執行這個函數, 那么就是相當于:

timer = timer(parameter)

task1 = timer(task1)

后面的運行就和一般的裝飾器一樣了:

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()

至此,裝飾器的全部內容結束。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容