1. 程式人生 > >Python高階程式設計——裝飾器Decorator詳解(上篇)(絕對是我見過最詳細的的教程,沒有之一哦)

Python高階程式設計——裝飾器Decorator詳解(上篇)(絕對是我見過最詳細的的教程,沒有之一哦)

一、先從一種情況開始看起

1、裝飾器decorator的由來
裝飾器的定義很是抽象,我們來看一個小例子。
先定義一個簡單的函式:

def myfunc:
    print('我是函式myfunc')

myfunc()  #呼叫函式

然後呢,我想看看這個函式執行這個函式用了多長時間,好吧,那麼我們可以這樣做:

import time
def myfunc:
    start = time.clock()  
    print('我是函式myfunc')
    end = time.clock()
    print(f'函式所花費的時間為 :{end - start}')

myfunc()  #函式呼叫

我們現在已經達到了我們的目的。但是如果是我們還想繼續給另外的一些函式也實現同樣的功能。那我們是不是給每個函式都新增這麼幾句話呢?當然可以,但是不高效,而且很麻煩。如果有某一種方式可以一次性解決所有的問題,那自然最好不過了,於是“裝飾器”就應運而生。

在上面的例子中,函式本身的功能只是列印一句話而已,但是經過改造後的函式不僅要能夠列印這一句話,還要能夠顯示函式執行所花費的時間,這相當於我要給這個函式新增額外的功能,注意這個關鍵字,其實“裝飾器”就是專門給函式新增額外的功能的。

2、新增額外功能的簡單實現——非“裝飾器”實現
還記得嗎,函式在Python中是一等公民,那麼我們可以考慮重新定義一個函式timeit,將myfunc的引用傳遞給他,然後在timeit中呼叫myfunc並進行計時,這樣,我們就達到了不改動myfunc定義但是又添加了額外功能

的目的,程式碼如下:

import time

def myfunc():

    print("我是函式myfunc")

def timeit(function):
    start = time.clock()
    function()
    end =time.clock()
    print(f'函式執行所花費的時間為:{end-start}')

timeit(myfunc)

只行結果為:

我是函式myfunc
函式執行所花費的時間為:0.0004924657368762765

上面的程式碼看起來邏輯上並沒有問題,也達到了我們所要實現的目的!但是,我們雖然沒有修改函式myfunc定義中的程式碼,但是我們似乎修改了呼叫部分的程式碼。原本我們是這樣呼叫的:myfunc(),修改以後變成了:timeit(myfunc)。這樣的話,如果myfunc在N處都被呼叫了,你就不得不去修改這N處的程式碼。或者更極端的,考慮其中某處呼叫的程式碼無法修改這個情況,比如:這個函式是你交給別人使用的。
其實將函式作為引數傳遞,已經具備了裝飾器的雛形

了,但是上面的實現還不夠好,下面會給出更好地實現方式。 
 

二、什麼是裝飾器——decorator

一般而言,如果我需要給函式新增額外的某一些功能,我需要修改函式的原始碼,但是如前面所說,這樣麻煩,而且不高效,裝飾器就是專門的解決方案!

1、什麼是裝飾器?——兩個層面

在Python裡面有兩層定義:

第一:從設計模式的層面上

裝飾器是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的應用有插入日誌、增加計時邏輯來檢測效能、加入事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函式中與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。

第二:從Python的語法層面上(其實第二種本質上也是第一種,只不過在語法上進行了規範化)

簡言之,python裝飾器就是用於拓展原來函式功能的一種函式,這個函式的特殊之處在於它的返回值也是一個函式,使用python裝飾器的好處就是在不用更改原函式的程式碼前提下給函式增加新的功能 如此一來,我們要想拓展原來函式程式碼,就不需要再在函式裡面修改原始碼了。

2、裝飾器的作用——兩方面

(1)抽離雷同程式碼,加以重用

(2)為函式新增額外的功能

3、裝飾器的使用場景

(1)快取裝飾器

(2)許可權驗證裝飾器

(3)計時裝飾器

(4)日誌裝飾器

(5)路由裝飾器

(6)異常處理裝飾器

(7)錯誤重試裝飾器

三、裝飾器的實現

1、裝飾器的逐步實現

針對上面改進版的程式碼所存在的哪些問題,我們想出瞭解決辦法:

既然修改N處的呼叫程式碼很麻煩,我們就來想想辦法不修改呼叫程式碼;如果不修改呼叫程式碼,也就意味著呼叫myfunc()需要產生呼叫timeit(myfunc)的效果。

因為python中一切皆物件,故而我們可以想到將timeit賦值給myfunc,

程式碼如下:

import time

def myfunc():

    print("我是函式myfunc")

def timeit(function):
    start = time.clock()
    function()
    end =time.clock()
    print(f'函式執行所花費的時間為:{end-start}')

myfunc=timeit  #將timeit賦值給原來的myfunc
myfunc()

但是上面的呼叫並不會成功,會顯示出如下錯誤:

timeit() missing 1 required positional argument: 'function'

這是因為將timeit賦值給myfunc之後,此時myfunc和timeit表示的同一個東西,但是timeit似乎帶有一個引數function,而在呼叫myfunc()的時候並沒有傳入任何引數,所以並不會成功。

但是上面的呼叫雖然沒有成功,卻給我們指出了一條重要的線索,因為上面的程式碼已經解決“修改呼叫程式碼”的問題,只不過是引數沒有統一而已,那就想辦法把引數統一吧!那就再新增一個函式唄!什麼意思?

因為引數不統一,如果timeit()不併不是直接新增額外的功能,而是返回一個與myfunc引數列表一致的函式。而原來timeit需要新增額外功能的程式碼再在timeit裡面定義一個函式,由它去完成不就可以了嗎,將timeit(myfunc)的返回值賦值給myfunc,然後,呼叫myfunc()的程式碼完全不用修改。——即我們依然是呼叫myfunc(呼叫程式碼沒變),但是同樣卻達到了新增額外功能的效果

程式碼如下:

import time
#原來的函式myfunc
def myfunc():
    print("我是函式myfunc")

#定義一個計時器
def timeit(function):
    '''
       timeit函式負責返回一個wrapper,wrapper的引數要與原來的myfunc保持相同
       這樣一來,執行 myfunc=timeit(myfunc)  myfunc完全等價於wrapper

       wrapper函式負責新增額外功能
    '''
    def wrapper():
        start = time.clock()
        function()
        end =time.clock()
        print(f'函式執行所花費的時間為:{end-start}')
    return wrapper

myfunc=timeit(myfunc)  #注意,這裡與前面的 “myfunc=timeit”是有所區別的哦
myfunc()  #還和原來呼叫myfunc()一樣,但是達到了新增額外功能的效果

上面的執行結果就出來了,如下:

我是函式myfunc
函式執行所花費的時間為:0.0005973331798019136
總結:在上面的函式定義和呼叫中,看起來我的呼叫myfunc()和原來並沒有任何不同,但是卻已經添加了額外的效果。它解決前面存在的兩個問題

(1)不用修改函式原始碼,也不用修改呼叫函式的程式碼,完全跟呼叫最原始的myfunc()程式碼一樣,但是卻添加了額外功能;

(2)解決了timeit和myfunc的引數不統一問題,那就是再新增一層wrapper;

——這就是裝飾器。

上面的裝飾器就是最原始的版本,但是python中引入了專門的“語法糖”來實現裝飾器,這樣看起來更加專業,更加美觀。就是使用字元“@”去實現。程式碼如下:

import time

#定義一個計時器
def timeit(function):
    '''
       timeit函式負責返回一個wrapper,wrapper的引數要與原來的myfunc保持相同
       這樣一來,執行 myfunc=timeit(myfunc)  myfunc完全等價於wrapper

       wrapper函式負責新增額外功能
    '''
    def wrapper():
        start = time.clock()
        function()
        end =time.clock()
        print(f'函式執行所花費的時間為:{end-start}')
    return wrapper

#myfunc=timeit(myfunc)  #注意,這裡與前面的 “myfunc=timeit”是有所區別的哦

#原來的函式myfunc
@timeit
def myfunc():
    print("我是函式myfunc")

myfunc()  #還和原來呼叫myfunc()一樣,但是達到了新增額外功能的效果

上面程式碼的執行結果依然是:

我是函式myfunc
函式執行所花費的時間為:0.0004893814003196401

在上面的例子中,在定義myfunc函式的上面加了一個@timeit,這與前面的寫法myfunc = timeit(myfunc)完全等價,

@有兩個重要的作用,第一:較少了程式碼書寫量;第二:那就是讓我們的程式碼看上去更有裝飾器的感覺,看起來更高端了。

總結:

在這個例子中,函式進入和退出時需要計時,這被稱為一個橫切面(Aspect),這種程式設計方式被稱為面向切面的程式設計(Aspect-Oriented Programming)。與傳統程式設計習慣的從上往下執行方式相比較而言,像是在函式執行的流程中橫向地插入了一段邏輯。在特定的業務領域裡,能減少大量重複程式碼。面向切面程式設計還有相當多的術語,這裡就不多做介紹,感興趣的話可以去找找相關的資料(如果有需要,我後面也會抽時間專門寫一系列關於面向切面程式設計的文章,看我有沒有時間啦!)

2、裝飾器的一般結構

為了能夠明確裝飾器的實現原理,這裡給出一個關於裝飾器的“一般模板”,方便大家理解!但是,裝飾器作為一種設計模式,本身是沒有固定的設計模板的,語法也是相對較為靈活,沒有說一定要怎麼寫才正確。

模板如下:

def decorator(function):
    '''
    第一層函式為裝飾器名稱
    function:引數,即需要裝飾的函式
    return:返回值wrapper,為了保持與原函式引數一致
    '''
    def wrapper(*arg,**args):
        '''
           內層函式,這個函式實現“新增額外功能”的任務
           *arg,**args:引數保持與需要裝飾的函式引數一致,這裡用*arg和**args代替
        '''
        #這裡就是額外功能程式碼
        function()   #執行原函式
        #這裡就是額外功能程式碼
    return wrapper

一般就按照上面這個模板寫“裝飾器”函式,一般就不會出錯了。
 

四、裝飾器的各種花式實現

學過裝飾器的人都知道Python的閉包,關於“閉包”的詳細定義有各種版本,但我們經常看見這樣一句話,“Python的裝飾器就是一種閉包或者是Python的閉包其實就是裝飾器”,這句話在一定程度上是不正確的,但是這麼說也可以(心裡要明白二者的本質)。

本質:python閉包是裝飾器的真子集,即裝飾器是更加寬泛的概念,至於為什麼,它們二者的區別和聯絡,我會在

Python高階程式設計——裝飾器Decorator詳解(上篇)

中繼續講解python閉包和裝飾器的區別和聯絡。

不僅如此,上面所實現的裝飾器是針對函式的,實際上Python的裝飾器可以是“函式”或者是“類”,而被裝飾的物件也可以是“函式”或者是“類”,這樣一來,就有四種搭配情況,即:

函式裝飾函式

函式裝飾類

類裝飾函式

類裝飾類

具體每一種怎麼實現呢?其實他們的設計思想都是大同小異,只是實現細節略有不同,欲知詳細情況,且聽下回分解!!!

下一篇預告:

裝飾器與閉包的聯絡和區別

四大類裝飾器的搭配實現