第20天:Python 之裝飾器


                                                                 

圖片

1. 概念介紹

裝飾器(decorator),又稱「裝飾函數」,即一種返回值也是函數的函數,能夠稱之爲「函數的函數」。其目的是在不對現有函數進行修改的狀況下,實現額外的功能。最基本的理念來自於一種被稱爲「裝飾模式」的設計模式。html

在 Python 中,裝飾器屬於純粹的「語法糖」,不使用也不要緊,可是使用的話可以大大簡化代碼,使代碼更加易讀——固然,是對知道這是怎麼回事兒的人而言。python

想必通過一段時間的學習,大機率已經在 Python 代碼中見過@這個符號。沒錯,這個符號正是使用裝飾器的標識,也是正經的 Python 語法。程序員

語法糖:指計算機語言中添加的某種語法,這種語法對語言的功能並無影響,可是更方便程序員使用。一般來講使用語法糖可以增長程序的可讀性,從而減小程序代碼出錯的機會。編程

2. 運行機制

簡單來講,下面兩段代碼在語義上是能夠劃等號的(固然具體過程仍是有一點微小區別的):設計模式

def IAmDecorator(foo):    '''我是一個裝飾函數'''    pass
@IAmDecoratordef tobeDecorated(...):    '''我是被裝飾函數'''    pass

與:數據結構

def IAmDecorator(foo):    '''我是一個裝飾函數'''    pass
def tobeDecorated(...):    '''我是被裝飾函數'''    passtobeDecorated = IAmDecorator(tobeDecorated)

能夠看到,使用裝飾器的@語法,就至關因而將具體定義的函數做爲參數傳入裝飾器函數,而裝飾器函數則通過一系列操做,返回一個新的函數,而後再將這個新的函數賦值給原先的函數名。app

最終獲得的是一個與咱們在代碼中顯式定義的函數同名異質的新函數。ide

而裝飾函數就好像爲原來的函數套了一層殼。如圖所示,最後獲得的組合函數即爲應用裝飾器產生的新函數:函數式編程

圖片

這裏要注意一點,上述兩段代碼在具體執行上仍是存在些微的差別。在第二段代碼中,函數名tobeDecorated其實是先指向了原函數,在通過裝飾器修飾以後,才指向了新的函數;但第一段代碼的執行就沒有這個中間過程,直接獲得的就是名爲tobeDecorated的新函數。函數

此外,裝飾函數有且只能有一個參數,即要被修飾的原函數。

3. 用法

Python 中,裝飾器分爲兩種,分別是「函數裝飾器」和「類裝飾器」,其中又以「函數裝飾器」最爲常見,「類裝飾器」則用得不多。

3.1 函數裝飾器

3.1.1 大致結構

對裝飾函數的定義大體能夠總結爲如圖所示的模板,即:

圖片

因爲要求裝飾函數返回值也爲一個函數的緣故,爲了在原函數的基礎上對功能進行擴充,而且使得擴充的功能可以以函數的形式返回,所以須要在裝飾函數的定義中再定義一個內部函數,在這個內部函數中進一步操做。最後return的對象就應該是這個內部函數對象,也只有這樣纔可以正確地返回一個附加了新功能的函數。

如圖一的動圖所示,裝飾函數就像一個「包裝」,將原函數裝在了裝飾函數的內部,從而經過在原函數的基礎上附加功能實現了擴展,裝飾函數再將這個新的總體返回。同時對於原函數自己又不會有影響。這也是「裝飾」二字的含義。

這個地方若是不定義「內部函數」行不行呢?

答案是「不行」。

3.1.2 關於結構的解釋

讓咱們來看看下面這段代碼:

>>> def IAmFakeDecorator(fun):...     print("我是一個假的裝飾器")...     return fun...>>> @IAmFakeDecorator... def func():...     print("我是原函數")...我是一個假的裝飾器

有點奇怪,怎麼剛必定義,裝飾器擴展的操做就執行了呢?

再來調用一下新函數:

>>> func()我是原函數

誒呦奇了怪了,擴展功能哪兒去了呀?

不要着急,咱們來分析一下上面的代碼。在裝飾函數的定義中,咱們沒有另外定義一個內部函數,擴展操做直接放在裝飾函數的函數體中,返回值就是傳入的原函數。

在定義新函數的時候,下面兩段代碼又是等價的:

>>> @IAmFakeDecorator... def func():...     print("我是原函數")...我是一個假的裝飾器

>>> def func():...     print("我是原函數")...>>> func = IAmFakeDecorator(func)我是一個假的裝飾器

審視一下後一段代碼,咱們能夠發現,裝飾器只在定義新函數的同時調用一次,以後新函數名引用的對象就是裝飾器的返回值了,與裝飾器沒有半毛錢關係。

換句話說,裝飾器自己的函數體中的操做都是當且僅當函數定義時,纔會執行一次,之後再以新函數名調用函數,執行的只會是內部函數的操做。因此到實際調用新函數的時候,獲得的效果跟原函數沒有任何區別。

若是不定義內部函數,單純返回傳入的原函數固然也是能夠的,也符合裝飾器的要求;但卻得不到咱們預期的結果,對原函數擴展的功能沒法複用,只是一次性的。所以這樣的行爲沒有任何意義。

這個在裝飾函數內部定義的用於擴展功能的函數能夠隨意取名,但通常約定俗成命名爲wrapper,即「包裝」之意。

正確的裝飾器定義應以下所示:

>>> def IAmDecorator(fun):...     def wrapper(*args, **kw):...         print("我真的是一個裝飾器")...         return fun(*args, **kw)...     return wrapper...

3.1.3 參數設置的問題

內部函數參數設置爲(*args, **kw)的目的是能夠接收任意參數,關於如何接收任意參數的內容在前面的函數參數[1]部分已經介紹過。

之因此要讓wrapper可以接收任意參數,是由於咱們在定義裝飾器的時候並不知道會用來裝飾什麼函數,具體函數的參數又是什麼狀況;定義爲「能夠接收任意參數」可以極大加強代碼的適應性。

另外,還要注意給出參數的位置。

要明確一個概念:除了函數頭的位置,其餘地方一旦給出了函數參數,表達式的含義就再也不是「一個函數對象」,而是「一次函數調用」。

所以,咱們的裝飾器目的是返回一個函數對象,返回語句的對象必定是不帶參數的函數名;在內部函數中,咱們是須要對原函數進行調用,所以須要帶上函數參數,不然,若是內部函數的返回值仍是一個函數對象,就還須要再給一組參數纔可以調用原函數。Show code:

>>> def IAmDecorator(fun):...     def wrapper(*args, **kw):...         print("我真的是一個裝飾器")...         return fun...     return wrapper...>>> @IAmDecorator... def func(h):...     print("我是原函數")...>>> func()我真的是一個裝飾器<function func at 0x000001FF32E66950>

原函數沒有被成功調用,只是獲得了原函數對應的函數對象。只有進一步給出了下一組參數,纔可以發生正確的調用(爲了演示參數的影響,在函數func的定義中增長了一個參數h):

>>> func()(h=1)我真的是一個裝飾器我是原函數

只要明白了帶參數和不帶參數的區別,而且知道你想要的究竟是什麼效果,就不會在參數上犯錯誤了。而且也徹底沒必要拘泥上述規則,也許你要的就是一個未經調用的函數對象呢?

把握住這一點,嵌套的裝飾器、嵌套的內部函數這些也就都不是問題了。

3.1.4 函數屬性

本小節內容啓發於廖雪峯的官方網站-Python 教程-函數式編程-裝飾器[2]

還應注意的是,通過裝飾器的修飾,原函數的屬性也發生了改變。

>>> def func():...     print("我是原函數")...>>> func.__name__'func'

正常來講,定義一個函數,其函數名稱與對應的變量應該是一致的,這樣在一些須要以變量名標識、索引函數對象時纔可以避免沒必要要的問題。可是事情並非那麼順利:

>>> @IAmDecorator... def func():...     print("我是原函數")...>>> func.__name__'wrapper'

變量名仍是那個變量名,原函數仍是那個原函數,可是函數名稱卻變成了裝飾器中內部函數的名稱。

在這裏咱們可使用 Python 內置模塊functools中的wraps工具,實現「在使用裝飾器擴展函數功能的同時,保留原函數屬性」這一目的。這裏functools.wraps自己也是一個裝飾器。運行效果以下:

>>> import functools>>> # 定義保留原函數屬性的裝飾器... def IAmDecorator(fun):...     @functools.wraps(fun)...     def wrapper(*args, **kw):...         print("我真的是一個裝飾器")...         return fun(*args, **kw)...     return wrapper...>>> @IAmDecorator... def func():...     print("我是原函數")...>>> func.__name__'func'

大功告成!

3.2 類裝飾器

本節部分參考[Python3 文檔-複合語句-類定義[3]]和[python 一篇文章搞懂裝飾器全部用法[4]]中類裝飾器相關部分

類裝飾器的概念與函數裝飾器相似,使用上語法也差很少:

@ClassDecoratorclass Foo:    pass

等價於

class Foo:    passFoo = ClassDecorator(Foo)

在定義類裝飾器的時候,要保證類中存在__init____call__兩種方法。其中__init__方法用以接收原函數或類,__call__方法用以實現裝飾邏輯。

簡單來說,__init__方法負責在初始化類實例的時候,將傳入的函數或類綁定到這個實例上;而__call__方法則與通常的函數裝飾器差很少,連構造都沒什麼兩樣,能夠認爲__call__方法就是一個函數裝飾器,所以再也不贅述。

3.3 多個裝飾器的狀況

多個裝飾器能夠嵌套,具體狀況能夠理解爲從下往上結合的複合函數;或者也能夠理解爲下一個裝飾器的值是前一個裝飾器的參數。

舉例來講,下面兩段代碼是等價的:

@f1(arg)@f2def func():     pass

def func():     passfunc = f1(arg)(f2(func))

理解了前面的內容,這種狀況也很容易掌握。

4. 總結

本文介紹了 Python 中的裝飾器這一特性,詳細講解了裝飾器的實際原理和使用方式,可以大大幫助學習者掌握有關裝飾器的知識,減少讀懂 Python 代碼的阻力,寫出更加 pythonic 的代碼。

參考

[1] Python3 術語表-裝飾器[5]

[2] Python3 文檔-複合語句-函數定義[6]

[3] Python3 文檔-複合語句-類定義[7]

[4] 語法糖[8]

[5] 廖雪峯的官方網站-Python 教程-函數式編程-裝飾器[9]

參考資料

[1]

函數參數: http://www.justdopython.com/2019/09/19/python-function-param-017/

[2]

廖雪峯的官方網站-Python 教程-函數式編程-裝飾器: https://www.liaoxuefeng.com/wiki/1016959663602400/1017451662295584

[3]

[Python3 文檔-複合語句-類定義: https://docs.python.org/3/reference/compound_stmts.html#class-definitions

[4]

[python 一篇文章搞懂裝飾器全部用法: https://www.jb51.net/article/168276.htm

[5]

1] [Python3 術語表-裝飾器: https://docs.python.org/3/glossary.html#term-decorator

[6]

2] [Python3 文檔-複合語句-函數定義: https://docs.python.org/3/reference/compound_stmts.html#function-definitions

[7]

3] [Python3 文檔-複合語句-類定義: https://docs.python.org/3/reference/compound_stmts.html#class-definitions

[8]

4] [語法糖: https://baike.baidu.com/item/%E8%AF%AD%E6%B3%95%E7%B3%96/5247005?fr=aladdin

[9]

5] [廖雪峯的官方網站-Python 教程-函數式編程-裝飾器: https://www.liaoxuefeng.com/wiki/1016959663602400/1017451662295584

系列文章   第11天:Python 字典

第10天:Python 類與對象

第9天:Python Tupple

第8天:Python List

第7天:Python 數據結構--序列

第6天:Python 模塊和

第5天:Python 函數

第4天:Python 流程控制

第3天:Python 變量與數據類型

第2天:Python 基礎語法

第1天:Python 環境搭建

相關文章
相關標籤/搜索