Python裝飾器的通俗理解

來源:互聯網
上載者:User

標籤:python   裝飾器   python裝飾器   

在學習Python的過程中,我相信有很多人和我一樣,對Python的裝飾器一直覺得很困惑,我也是困惑了好久,並通過思考和查閱才能略有領悟,我希望以下的內容會對你有協助,我也努力通過通俗的方式使得對Python裝飾器的理解更加的透徹,很多人對裝飾器難以理解,原因是由於以下三點內容沒有搞清楚:


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

關於高階函數的理解

關於嵌套函數的理解


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


1、裝飾器

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


不能修改被裝飾的函數的原始碼

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

滿足以上的情況下給程式增添功能

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


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


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

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


2、需求的實現

假設有代碼:

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

很顯然,這段代碼啟動並執行結果一定是:等待約2秒後,輸出

test is running

那麼要求在滿足三原則的基礎上,給程式添加統計已耗用時間(2 second)功能

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


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

假設有代碼:

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

那麼在記憶體中,應該是這樣的:


很顯然,函數和變數是一樣的,都是“一個名字對應記憶體位址中的一些內容” 

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


test1表示的是函數的記憶體位址

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


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


2.2高階函數

那麼對於高階函數的形式可以有兩種:


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

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

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


還是針對上面那段代碼:


import timedef 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()這條語句,那麼就用到了第二種高階函數,即傳回值中包含函數名


如下代碼:


import timedef test():    time.sleep(2)    print("test is running!")def deco(func):      print(func)    return func t = deco(test) #3#t()#4test()

我們看這段代碼,在#3處,將test傳入deco(),在deco()裡面操作之後,最後返回了func,並賦值給t。因此這裡test => func => t,都是一樣的函數體。最後在#4處保留了原來的函數調用方式。 

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


2.3 嵌套函數

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


def func1():

    def func2():

        pass

而不是

def func1():

    func2()

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


那麼我們再回到我們之前的那個需求,想要統計程式已耗用時間,並且滿足三原則。


代碼:


import timedef timer(func) #5    def deco():          start = time.time()        func()        stop = time.time()        print(stop-start)    return deco    def test():    time.sleep(2)    print("test is running!")    test = timer(test) #6   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、裝飾有參函數

import timedef timer(func)    def deco():          start = time.time()        func()        stop = time.time()        print(stop-start)    return deco    @timerdef 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。


完整代碼如下:


import timedef timer(func)    def deco(*args, **kwargs):          start = time.time()        func(*args, **kwargs)        stop = time.time()        print(stop-start)    return deco    @timerdef 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處返回。


完整程式為:


import timedef timer(func)    def deco(*args, **kwargs):          start = time.time()        res = func(*args, **kwargs)        stop = time.time()        print(stop-start)        return res     return deco@timerdef 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 timedef 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()

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


Python裝飾器的通俗理解

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.