大話python裝飾器

由於裝飾器的結構和使用形式,相信很多python的初學者在學習的過程中有很多困惑,本文儘量站在初學者的角度,用大白話和簡單的代碼對裝飾器進行講解,繞開閉包和對象引用的概念,希望儘可能減少初學者在學習裝飾器時的困惑。

1 什麼是裝飾器

其實對於初學者來說,最大的疑惑可能是裝飾器是幹什麼用的?爲什麼我在編程的過程中基本上用不到,我在什麼場合下必須用它呢?

其實裝飾器很簡單,從名字上就可以看出它的功能,它就是裝飾用的。而它的裝飾對象就是函數,而所謂的“裝飾”就是給函數添加功能的意思。

這個時候你可能會想,給函數添加功能,直接修改函數不就行了,爲啥要用裝飾器呢。事實上確實是這樣,用裝飾器能完成的功能,直接修改函數也能達到同樣的效果,這也是爲什麼初學者基本上用不到裝飾器的原因。但是在實際的項目的某個階段,你可能不允許改動測試好的函數,而想增加一些功能,比如統計這個函數運行的時間,那麼這個時候,裝飾器就派上用場了。這也恰恰是裝飾器的本質所在,在不改變函數和它的引用的情況下,增加函數的功能。

那麼基於以上的分析,假設現在我們有如下的函數,其作用是在兩秒後,輸出打印的內容。在這個函數的基礎上,我們提出以下需求,後文將在解決這個需求的過程中,逐步深入裝飾器的原理。

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

print_function() #1

1、給該函數增加一個運行耗時統計的功能

2、不改動這個函數的內部代碼

3、不改動這個函數的引用

好,如果你沒有思考過這個問題,可以在這裏停下來想一想再繼續下面的內容。

2 問題的解決

現在我們想要增加一個統計這個函數運行時間的功能,而不改動函數的源碼,首先我們想到的是,在這個函數引用(#1 處)的外部,添加這些計時功能的代碼,就像下面這樣:

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

time_start=time.time()
print_function() #1
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))

可以看到,通過在 #1處的上下增加代碼,我們實現了函數的計數功能,而且沒有改變函數的內部代碼。那麼這些增加的計時所用的代碼就是所謂的“裝飾”,其實裝飾器的原理就是這麼簡單。但是這種方式增加的代碼,讓函數的引用變得更加複雜。現在我們換一種方式,讓代碼看起來更加優雅。很自然的一個想法是,我們把最後四行再封裝成一個函數,去引用它。

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

def print_function_time():
    time_start=time.time()
    print_function() #1
    time_end=time.time()
    print("print function uses %f seconds"%(time_end-time_start))

print_function_time()

很顯然,print_function_time() 就是我們要的函數,但是由於它的函數名改變了,不符合我們一開始提出的問題。那有沒有什麼辦法不改名函數名,也能實現同樣的功能呢,答案就是高階函數。其實不要被高階函數這個名字唬住了,它和普通函數唯一的區別就在於它返回的不是數值、字符串、列表等這樣普通的對象,它返回的是一個函數。我們知道,當我們使用下面這條語句之後,f() 和print_function() 其實是等價的。

f=print_function()

那麼我們自然想到,把我們要“裝飾”的函數(print_function)傳到一個函數裏面,增加計時功能之後,再把它當做返回值返回,廢話不多說,上代碼。

import time 
def print_function():
    time.sleep(2)
    print("this is a print function")

def timer(func): #1
    def deco():
        time_start=time.time()
        func() #2
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
    return deco

print_function=timer(print_function) #3
print_function()

#1 處我們定義了一個裝飾函數,#2 處爲傳入的被裝飾的函數,最外層的timer函數返回的是 deco函數,也就是“裝飾後”的 func 函數。

因此我們在 #3處 引用 timer(print_function) 其實就是引用了deco(),也就是被裝飾後的print_function 函數,最後一行中print_function()的引用雖然和之前定義的函數名相同,但實際上,他已經在原來的基礎上增加了計時功能。

至此,我們已經實現了開頭提出來的問題,在不改變函數內部代碼和函數引用的情況下,增加計時功能。這就是一個裝飾器的工作原理和過程,其中print_function 我們可以稱之爲被裝飾的對象(函數),外部的 timer 就是裝飾器,當函數被當成參數傳入到裝飾器中,裝飾器返回一個被裝飾之後的函數,這個過程我們可以稱之爲裝飾。

當然python 提供了一種比較直觀的裝飾器語法,來代替上面這種樸素的寫法,使代碼更加優雅。

3 python 中裝飾器的寫法

在我們上面的寫法中,需要在每個被裝飾的函數前面加上一句

print_function=timer(print_function)

python 提供了一種更加簡潔的寫法。在需要裝飾的函數定義(#1 處)上面加 @timer 語句,就表示該函數被timer函數裝飾,注意,在引用裝飾函數(@timer)之前,需要先定義裝飾函數,不然會報錯。下面這段代碼是python 裝飾器的標準寫法,但是它和第2小節中的那段代碼是等效的。

import time 

def timer(func):
    def deco():
        time_start=time.time()
        func()
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
    return deco

@timer # 1
def print_function():
    time.sleep(2)
    print("this is a print function")

    
print_function()

4 裝飾有返回值的函數和帶有參數的函數

相信看到這,你已經對裝飾器的原理和作用有了一個初步的理解。在上面我們被裝飾的函數是一個沒有返回值和沒有參數的函數,那麼假如要裝飾有返回值和有參數的函數,該怎麼做呢,首先,我們來看有返回值的函數。

4.1 裝飾有返回值的函數

裝飾有返回值的函數其實很簡單,在上面一段代碼中,其實我們最後引用的print_function() 其實就是 deco()函數,顯然目前deco 函數是沒有返回值的,所以需要在deco函數中添加一個 return 項,那麼要return 什麼呢?因爲我們要的是被裝飾函數(print_function)的返回值,其實就是要返回其內部 func()的返回值,所以需要把func()的返回值記錄下來,然後在deco 函數中返回:

import time 

def timer(func):
    def deco():
        time_start=time.time()
        value=func()
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
        return value
    return deco

@timer # 1
def print_function():
    time.sleep(2)
    print("this is a print function")
    return "run over"

print_function() #1  輸出 'run over'

4.2 返回帶有參數的函數

實際中的函數往往是有參數的,如果將上面被裝飾的print_function 函數定義處直接改成有參函數,如下面的代碼所示,顯然這段代碼是會報錯的,因爲在裝飾函數中傳入的函數是沒有參數的函數。

import time 

def timer(func):
    def deco():
        time_start=time.time()
        value=func() 
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
        return value
    return deco

@timer 
def print_function(parameter): #2 增加形參
    time.sleep(2)
    print("this is a print function")
    return "run over"

print_function() 

爲了裝飾帶有參數的函數,需要給裝飾器中的deco()和func()也加上參數,考慮到參數數量的不確定性和不同類型(普通參數和關鍵字參數),參數採用可變參數(**args)和可變關鍵字參數(**args)兩者的組合,這樣就可以適應有各種參數的函數。

import time 

def timer(func):
    def deco(*args,**kwargs):
        time_start=time.time()
        value=func(*args,**kwargs) 
        time_end=time.time()
        print("print function uses %f seconds"%(time_end-time_start))
        return value
    return deco

@timer 
def print_function(parameter): #2 增加形參
    time.sleep(2)
    print("this is a print function",parameter)
    return "run over"

print_function('with parameter')  # 輸出 this is a print function with parameter

5 帶有參數的裝飾器

有時候,同一個裝飾器,針對不同的輸入函數要採取不同的裝飾方式。因此就需要有參數來標記對哪個函數採取哪種裝飾措施,這種帶有參數的裝飾器的使用如下:

@timer(parameter=parmeter_value)

比如我們有兩個print_function,分別是print_function1 和 print_function2,如下所示:

def print_function1(): 
    time.sleep(1)
    print("this is the print function 1")

def print_function2()
    time_sleep(2)
    print(this is the print function 2) 

針對不同的打印函數,除了都需要計時外,裝飾器還要針對傳入的兩個不同函數輸出“in print_function1”和“in print_function1/print_function2”,這個時候裝飾器就需要有參數來區分傳入的函數爲print_function1還是print_function2,不廢話,上代碼:

import time 

def timer(parameter):
    def outer(func): #1 增加一層函數
        def deco():
            print("in",parameter) #1 針對不同的輸出不同的語句
            time_start=time.time()
            func() 
            time_end=time.time()
            print("print function uses %f seconds"%(time_end-time_start))
        return deco
    return outer

@timer (parameter="print_function1")
def print_function1():
    time.sleep(1)
    print("this is the print function1")

@timer(parameter="print_function2")
def print_function2():
    time.sleep(2)
    print("this is the print function2")

print_function1()  # 輸出 in print_function1
print_function2()  # 輸出 in print_function2

由於要把paramter 參數傳遞到裝飾器中,我們以往都是將函數名傳入裝飾器,現在多了一個參數,要怎麼解決呢。一個自然的想法就是直接在原來timer(func)中直接增加一個參數,變爲time(func, parameter),但是這樣直接將兩個參數放到一起會引起後面引用的錯誤。

實際的做法就是在 #1 處增加一層函數來接受參數,其產生的效果如下所示:

timer=timer(parameter)
print_function=timer(print_function)

其運行就和一般的裝飾器一樣了。

6、進一步閱讀

其實裝飾器是閉包思想的一個應用,關於閉包的介紹,請參考談談自己的理解:python中閉包,閉包的實質

 

參考文章:

1、Python裝飾器的通俗理解

2、12步輕鬆搞定python裝飾器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章