什么是Python/ target=_blank class=infotextkey>Python裝飾器?
顧名思義,從字面意思可以理解為,它是用來"裝飾"Python的工具,使得代碼更具有Python簡潔的風格。裝飾器本質上是Python函數,能夠實現讓其他函數在不需要做任何代碼變動的前提下增加額外功能。
為什么用裝飾器?
裝飾器是通過某種方式來增強函數的功能。當然,我們可以通過很多方式來增強函數的功能,只是裝飾器有一個無可替代的優勢——簡潔且不改變函數內部代碼。
只需要在被修飾函數上方填加一個@修飾函數,就可以對這個函數增加額外功能。
裝飾器應用場景有哪些?
裝飾器最大的優勢是用于解決重復性的操作,其主要使用的場景有如下幾個:
- 日志輸出
- 類型檢查
- 鑒權(權限認證)
- 重試等等
當然,如果遇到其他重復操作的場景也可以從裝飾器的角度思考,如何提高代碼的簡潔性。通過可以抽離大量與函數功能本身無關的重復代碼到裝飾器中,概括的講,裝飾器的作用就是為已經存在的函數添加額外的功能。
簡單示例
下面就以一個簡單的例子來看一下它的作用。如果我們要對每個函數的增加重試機制,不使用裝飾器,通常會是這樣的,如下:
在ops函數里通過for遍歷以及try … except … else異常處理實現簡單重試,執行上述代碼,輸出結果為:
接下來,我們使用裝飾器,實現重試機制,具體實現如下:
通過編寫一個重試機制的裝飾器fun_retry,被修飾函數ops的作為裝飾器的參數,然后返回函數retry。
在fun_retry中嵌套了一個retry函數,那么retry是如何獲取fun這個參數來執行的?,可是retry并沒有接受fun這個傳參,這就是Python里的閉包的概念,閉包就是指運行時自帶上下文的函數,如這里的retry函數,它運行的時候自帶了上層函數fun_retry傳給他的fun這個函數,所以才可以在運行時對fun進行處理和輸出。
然后再每個被修飾函數上面加上@fun_retry來調用裝飾器,對不同的函數增加重試機制,即可省略每個函數里面的7行代碼,實現在函數不需要做任何代碼變動的前提下增加重試功能。
執行上述代碼,輸出結果為:
通過執行結果,也可以發現,當Python解釋器執行到@fun_retry時,就開始進行裝飾了,相當于執行了如下代碼:fun_retry(ops)。
帶參數的裝飾器
通過前面簡單的實例介紹,應該已經大致清楚裝飾器的作用和基本用法——通過閉包來實現裝飾器,函數(ops)作為外層函數(fun_retry)的傳入參數,然后在內層函數(retry)中運行、附加功能,隨后把內層函數作為結果逐層返回。
除了上述簡單的用法,裝飾器還有更高的靈活性,例如帶參數的裝飾器。
帶參數的修飾器并沒有太復雜,其實就是在上述基本的裝飾器的基礎上在外面套一層接收參數的函數。
比如,我們認為現有的重試機制靈活性很差,需要它更加靈活的進行重試,例如支持修改重試次數、重試等待時間等,這時候可以把重試次數、重試等待時間作為裝飾器的參數,如下:
可以看出,在原有的基礎上裝飾器外層又嵌套了一層函數fun_retry_more用來接收參數,這樣的話在ops函數前面調用時可以給裝飾器傳入參數,這樣的輸出結果是:
可能有人會有一個疑問,如果我的被修飾函數 ops 需要參數怎么辦?如果 ops 函數接收兩個、三個參數,甚至更多呢?當裝飾器不知道 ops 到底有多少個參數時,我們可以用*args 來代替,同樣對于關鍵字參數,我們可以使用**kwargs來代替,args是一個數組,kwargs一個字典,我們在上面例子中也有所體現,如def retry(args, **kwargs)。這樣它就能夠接受任意數量和類型的參數并把它們傳遞給被包裝的方法,使得我們能夠用這個裝飾器來裝飾任何方法。
對帶有返回值的函數進行裝飾
我們僅需要對retry閉包進行如上修改,將fun()的返回值賦給re_v,并且retry閉包中增加return 返回re_v即可,執行上述代碼,結果如下:
保留元信息的裝飾器
在修飾器中有一個細節很少有人提及,那就是保留被修飾對象的元信息的裝飾器
什么是函數的元信息?
就是函數本書的一些基本信息,例如函數名、函數文檔等,我們可以通過func.__name__獲取函數名、可以通過func.__doc__獲取函數的文檔信息,用戶也可以通過注解等方式為函數添加元信息。
使用裝飾器極大地復用了代碼,但是他有一個缺點就是原函數的元信息不見了,比如函數的docstring、name、參數列表,如下:
使用裝飾器極大地復用了代碼,但是他有一個缺點,就是被修飾函數的元信息丟失了,執行上述代碼,執行結果如下:
可以通過使用Python自帶模塊functools中的wraps來保留被修飾函數的元信息。
只需要在代碼中加入@wraps(fun)即可保留函數的元信息。執行上述代碼,輸出結果為:
裝飾器順序
一個函數還可以同時定義多個裝飾器,比如:
它的執行順序是從里到外,最先調用最里層的裝飾器,最后調用最外層的裝飾器,它等效于: