淺析Python裝飾器

一、什麼是裝飾器html

在介紹裝飾器以前,咱們先來思考一個問題:使用Python語言進行程序設計時,若是咱們想擴展一個函數的功能,通常會怎麼作呢?python

好比,有一個名爲print_info函數,當前該函數內只作一些簡單的打印操做,如今咱們想擴展這個函數功能,如在發生錯誤時,咱們將錯誤行號傳入到該函數打印出來。緩存

def print_info():
    print("Hello World")

看到這個問題,咱們的第一反應確定是從新修改這個函數。在print_info函數增長一個參數,用於接收外部傳入的錯誤行號,在函數內部增長打印行號信息語句。閉包

def print_info(err_line):
    print("Hello World")
    print("The Error Occurred Line Number: %d" %(err_line))

彷佛,經過修改函數內容,也能實現咱們的需求。但有沒有這種可能呢,不直接進行修改print_info函數內容,經過其餘的方式增長print_info函數的額外功能。答案確定是有的,能實現這種功能的就是咱們要講的裝飾器。
app

裝飾器究竟是什麼呢?使用比較嚴謹的語言來描述:函數

裝飾器本質上是一個Python函數,它可讓其餘函數在不須要作任何代碼變更的前提下增長額外功能,裝飾器的返回值也是一個函數對象。它常常用於有切面需求的場景,好比:插入日誌、性能測試、事務處理、緩存、權限校驗等場景。裝飾器是解決這類問題的絕佳設計,有了裝飾器,
咱們就能夠抽離出大量與函數功能自己無關的雷同代碼並繼續重用。

 歸納的講,裝飾器的做用就是爲已經存在的函數或對象添加額外的功能性能

二、怎麼寫一個裝飾器測試

咱們先來看一段這樣的代碼spa

 1 import sys
 2 
 3 def log(fcn):
 4     def wrapper(*argc, **kw):
 5         #New Operations Are Added Here
 6         if len(argc) != 1:
 7             print('Illegal parameter')
 8             return -1
 9         print("The Error Occurred Line Number: %d" %(argc[0]))
10         return fcn()
11     return wrapper      
12 
13 def print_info():
14     print("Hello World")
15 
16 print_info = log(print_info) 
17 
18 def main():
19     print_info(sys._getframe().f_lineno) # sys._getframe().f_lineno表明當前的行號
20 
21 if __name__ == "__main__":
22     main()# 添加功能並保持原函數名不變

log函數是一個「閉包」(關於什麼是「閉包」可參考「淺析Python閉包」),該函數有一個參數fcn,返回值是內部實現的wrapper函數。設計

比較重要的一條語句print_info = log(print_info),這裏經過對print_info變量賦值改變它原來指向的類型,起到了對print_info函數賦予了新功能。執行這句語句以後print_info變量再也不表示前面定義的print_info函數,而是表示log函數內實現的wrapper函數,但原來定義的print_info函數仍然存在。

最終調用的print_info函數,其實調用的是log中的wrapper函數。因爲「閉包」特性,雖然wrapper函數已經離開了創造它的環境log函數,但它仍然可使用log函數中的自由變量。wrapper函數中,打印了傳入的錯誤行號,並調用了傳入的原定義的print_info函數,這樣便作到了對定義的print_info函數內容作修改,調用print_info增長了額外功能。

運行結果

The Error Occurred Line Number: 19 # 打印出新增的對應行號信息,在19行被調用,因此傳入行號是19
Hello World #原print_info函數執行內容

上面的log函數其實已是一個裝飾器了,它對原函數作了包裝並返回了另一個函數,額外添加了一些功能。由於這樣寫實在不太優雅,新版本Python中支持了@語法糖,咱們能夠把它使用起來。

下面代碼等同於上面的寫法。

import sys

def log(fcn):
    def wrapper(*argc, **kw):
        #New Operations Are Added Here
        if len(argc) != 1:
            print('Illegal parameter')
            return -1
        print("The Error Occurred Line Number: %d" %(argc[0]))
        return fcn()
    return wrapper    
    
@log                      
def print_info():
    print("Hello World")

#print_info = log(print_info)

def main():
    print_info(sys._getframe().f_lineno)

if __name__ == "__main__":
    main()

把@log放到print_info函數定義處,至關於執行了print_info = log(print_info)語句

三、完整裝飾器的寫法

前面寫的裝飾器都沒有問題,可是還差最後一步,雖然裝飾器裝飾過的函數看上去名字沒變,其實已經變了。

def log(fcn):
    def wrapper(*args, **kw):
        print(fcn.__name__)
        return fcn()
    return wrapper  

@log
def print_info():
    print('Hello World')
    
def main():
    print_info()
    print(print_info.__name__)
    
if __name__ == "__main__":
    main()

運行結果

print_info
Hello World
wrapper
>>>

上面print_info函數經log裝飾器裝飾後,它的__name__屬性已經從原來的‘print_info’變成了‘wrapper’

由於返回的那個wrapper()函數名字就是'wrapper',因此,須要把原始函數的__name__等屬性複製到wrapper()函數中,不然,有些依賴函數簽名的代碼執行就會出錯。

不須要在內部添加wrapper.__name__=fcn.__name__這樣的代碼,Python內置的functools.wraps就是幹這個事的,因此,一個完整的decorator的寫法以下:

import functools

def log(fcn):
    @functools.wraps(fcn)
    def wrapper(*args, **kw):
        print(fcn.__name__)
        return fcn()
    return wrapper

四、高階一點裝飾器

4.1 帶參數裝飾器

若是裝飾器函數自己要傳入參數,那麼裝飾器就會是這樣的

import sys

def log(text):
    def wrapper(fcn)
        def inner_wrapper(*argc, **kw):
            #New Operations Are Added Here
            print(text)
            if len(argc) != 1:
                print('Illegal parameter')
                return -1
            print("The Error Occurred Line Number: %d" %(argc[0]))
            return fcn()
        return inner_wrapper
    return wrapper    
    
@log('With Parameter Decorator')
def print_info():
    print("Hello World")

#print_info = log(print_info)

def main():
    print_info(sys._getframe().f_lineno)

if __name__ == "__main__":
    main()

對於帶參數裝飾器,能夠這麼理解,當帶參數的裝飾器被裝飾在某個函數上時,好比上述代碼@log('With Parameter Decorator')log('With Parameter Decorator')實際上是一個函數,會立刻被執行,它返回的結果任是一個裝飾器wrapper。

也就是下面代碼其實等價於print_info = wrapper(print_info)

@log('With Parameter Decorator')
def print_info():
    print("Hello World")

4.2 裝飾器裝飾類方法

前面咱們介紹的都是用裝飾器裝飾函數,裝飾器同時也能夠裝飾類方法,下面來看如何用裝飾器裝飾類的方法

咱們定義了名爲simple_test的類,這個類有一個data屬性和get_data方法。類裏的實現很是簡單,get_data方法只返回data屬性值,並不進行其餘操做。同時,咱們還需定義了一個裝飾器裝飾類的get_data方法,裝飾器中根據數據獲取源來編寫其中功能,好比咱們這裏選擇從串口終端獲取數據,直接修改裝飾器的內容讓其獲取終端輸入數據。

這樣在設計一個類時,作到了將類中穩定的部分和常常要修改的部分獨立開。類設計完成後,咱們不須要更改類的源碼,在使用時只須要根據需求修改類方法的裝飾器就能實現咱們的需求。

def serial_data(fcn):
    def wrapper(self, *args, **kw):
        str = input('Please enter an integer: ')
        try:
            tmp = int(str)
        except ValueError:
            print('Invalid Value')
            return None
        self.data = tmp
        return fcn(self)
    return wrapper

class simple_test(object):
    def __init__(self, _val = 0):
        self.data = _val
    
    @serial_data
    def get_data(self):
        return self.data
    
def main():
    obj = simple_test()
    val = obj.get_data()
    if val is not None:
        print('get Data: %d' %(val))
    
if __name__ == "__main__":
    main() 

運行結果:

Please enter an integer: 100
get Data: 100
>>> 
相關文章
相關標籤/搜索