裝飾器(decorator),又稱“裝飾函數”,即一種返回值也是函數的函數,可以稱之為“函數的函數”。其目的是在不對現有函數進行修改的情況下,實現額外的功能。最基本的理念來自于一種被稱為“裝飾模式”的設計模式。
在 Python 中,裝飾器屬于純粹的“語法糖”,不使用也沒關系,但是使用的話能夠大大簡化代碼,使代碼更加易讀——當然,是對知道這是怎麼回事兒的人而言。
想必經過一段時間的學習,大概率已經在 Python 代碼中見過@這個符号。沒錯,這個符号正是使用裝飾器的标識,也是正經的 Python 語法。
語法糖:指計算機語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。
運行機制簡單來說,下面兩段代碼在語義上是可以劃等号的(當然具體過程還是有一點微小區别的):
def IAmDecorator(foo):
'''我是一個裝飾函數'''
pass
@IAmDecorator
def tobeDecorated(...):
'''我是被裝飾函數'''
pass
與:
def IAmDecorator(foo):
'''我是一個裝飾函數'''
pass
def tobeDecorated(...):
'''我是被裝飾函數'''
pass
tobeDecorated = IAmDecorator(tobeDecorated)
可以看到,使用裝飾器的@語法,就相當于是将具體定義的函數作為參數傳入裝飾器函數,而裝飾器函數則經過一系列操作,返回一個新的函數,然後再将這個新的函數賦值給原先的函數名。
最終得到的是一個與我們在代碼中顯式定義的函數同名而異質的新函數。
而裝飾函數就好像為原來的函數套了一層殼。如圖所示,最後得到的組合函數即為應用裝飾器産生的新函數:
這裡要注意一點,上述兩段代碼在具體執行上還是存在些微的差異。在第二段代碼中,函數名tobeDecorated實際上是先指向了原函數,在經過裝飾器修飾之後,才指向了新的函數;但第一段代碼的執行就沒有這個中間過程,直接得到的就是名為tobeDecorated的新函數。
此外,裝飾函數有且隻能有一個參數,即要被修飾的原函數。
用法Python 中,裝飾器分為兩種,分别是“函數裝飾器”和“類裝飾器”,其中又以“函數裝飾器”最為常見,“類裝飾器”則用得很少。
函數裝飾器
對裝飾函數的定義大緻可以總結為如圖所示的模闆,即:
由于要求裝飾函數返回值也為一個函數的緣故,為了在原函數的基礎上對功能進行擴充,并且使得擴充的功能能夠以函數的形式返回,因此需要在裝飾函數的定義中再定義一個内部函數,在這個内部函數中進一步操作。最後return的對象就應該是這個内部函數對象,也隻有這樣才能夠正确地返回一個附加了新功能的函數。
如圖一的動圖所示,裝飾函數就像一個“包裝”,将原函數裝在了裝飾函數的内部,從而通過在原函數的基礎上附加功能實現了擴展,裝飾函數再将這個新的整體返回。同時對于原函數本身又不會有影響。這也是“裝飾”二字的含義。
這個地方如果不定義“内部函數”行不行呢?
答案是“不行”。
關于結構的解釋
讓我們來看看下面這段代碼:
>>> 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
...
參數設置的問題
内部函數參數設置為(*args, **kw)的目的是可以接收任意參數,關于如何接收任意參數的内容在前面的函數參數部分已經介紹過。
之所以要讓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)
我真的是一個裝飾器
我是原函數
隻要明白了帶參數和不帶參數的區别,并且知道你想要的到底是什麼效果,就不會在參數上犯錯誤了。并且也完全不必拘泥上述規則,也許你要的就是一個未經調用的函數對象呢?
把握住這一點,嵌套的裝飾器、嵌套的内部函數這些也就都不是問題了。
函數屬性
還應注意的是,經過裝飾器的修飾,原函數的屬性也發生了改變。
>>> 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'
大功告成!
類裝飾器類裝飾器的概念與函數裝飾器類似,使用上語法也差不多:
@ClassDecorator
class Foo:
pass
等價于
class Foo:
pass
Foo = ClassDecorator(Foo)
在定義類裝飾器的時候,要保證類中存在__init__和__call__兩種方法。其中__init__方法用以接收原函數或類,__call__方法用以實現裝飾邏輯。
簡單來講,__init__方法負責在初始化類實例的時候,将傳入的函數或類綁定到這個實例上;而__call__方法則與一般的函數裝飾器差不多,連構造都沒什麼兩樣,可以認為__call__方法就是一個函數裝飾器,因此不再贅述。
多個裝飾器的情況多個裝飾器可以嵌套,具體情況可以理解為從下往上結合的複合函數;或者也可以理解為下一個裝飾器的值是前一個裝飾器的參數。
舉例來說,下面兩段代碼是等價的:
@f1(arg)
@f2
def func():
pass
和
def func():
pass
func = f1(arg)(f2(func))
理解了前面的内容,這種情況也很容易掌握。
總結本文介紹了 Python 中的裝飾器這一特性,詳細講解了裝飾器的實際原理和使用方式,能夠大大幫助學習者掌握有關裝飾器的知識,減小讀懂 Python 代碼的阻力,寫出更加 pythonic 的代碼。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!