Python之裝飾器

在學習Python的過程當中,我相信有不少人和我同樣,對Python的裝飾器一直以爲很困惑,我也是困惑了很久,並經過思考和查閱才能略有領悟,我但願如下的內容會對你有幫助,我也努力經過通俗的方式使得對Python裝飾器的理解更加的透徹。在文中若有遺漏和不足,歡迎交流和指點。
容許轉載並註明出處:http://blog.csdn.net/u013471155閉包

不少人對裝飾器難以理解,緣由是因爲如下三點內容沒有搞清楚:app

關於函數「變量」(或「變量」函數)的理解
關於高階函數的理解
關於嵌套函數的理解
那麼若是能對以上的問題一一攻破,同時遵循裝飾器的基本原則,相信會對裝飾器有個很好的理解的。那麼咱們先來看如下裝飾器的目的及其原則。函數

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

不能修改被裝飾的函數的源代碼
不能修改被裝飾的函數的調用方式
知足一、2的狀況下給程序增添功能
那麼根據需求,同時知足了這三點原則,這纔是咱們的目的。由於,下面咱們從解決這三點原則入手來理解裝飾器。spa

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

< 函數+實參高階函數+返回值高階函數+嵌套函數+語法糖 = 裝飾器 >
這個式子是貫穿裝飾器的靈魂所在!blog

二、需求的實現
假設有代碼:內存

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

很顯然,這段代碼運行的結果必定是:等待約2秒後,輸出作用域

test is runningtest

那麼要求在知足三原則的基礎上,給程序添加統計運行時間(2 second)功能
在行動以前,咱們先來看一下文章開頭提到的緣由1(關於函數「變量」(或「變量」函數)的理解)

2.一、函數「變量」(或「變量」函數)
假設有代碼:

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

 

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

三、 真正的裝飾器
根據以上分析,裝飾器在裝飾時,須要在每一個函數前面加上:

test = timer(test)

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

@timer

這兩句是等價的,只要在函數前加上這句,就能夠實現裝飾做用。

以上爲無參形式

四、裝飾有參函數
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()

五、帶參數的裝飾器
又增長了一個需求,一個裝飾器,對不一樣的函數有不一樣的裝飾。那麼就須要知道對哪一個函數採起哪一種裝飾。所以,就須要裝飾器帶一個參數來標記一下。例如:

@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()至此,裝飾器的所有內容結束。

相關文章
相關標籤/搜索