Python修飾器是個非常強大的概念,可以用一個函數去“包裝”另一個函數。修飾器的思想,就是把函數中除了正常行為之外的部分抽象出去。這樣有很多好處,如很容易進行代碼複用,並且能遵守科裡定律(即一次隻做一件事)。
學習怎樣編寫修飾器,可以大幅度增加代碼的可讀性。它能改變函數的行為,而無需實際去改變函數的代碼(如添加日誌行等)。
而修飾器是Python中非常常用的工具,用過Flask、Click等框架的人,都應該很熟悉修飾器,但許多人只知道怎麽用,卻不知道該怎麽寫。如果你也不知道怎麽寫,那這篇文章,正是為你準備的!
修飾器的原理
首先我們來看一個Python修飾器的例子。下面是個非常簡單的例子,演示了修飾器的使用方法。
在Python中定義的函數實際上是個對象。
上面的函數Hello是個函數對象。@my_decorator實際上也是個函數,它接受hello對象,並返回另一個對象給解釋器。修飾器返回的對象會成為實際的hello函數。
本質上這跟正常的函數一樣,如hello = decorate(hello)。我們傳給函數decorate一個函數,decorate可以隨便怎樣去用這個函數,然後返回另一個對象。修飾器可以乾脆吞掉那個函數,或者如果需要,還可以返回某個不是函數的東西。
編寫自己的修飾器
前面說過,修飾器就是個簡單的函數,它接受函數作為輸入,返回一個對象。因此,編寫修飾器實際上只需要定義一個函數。
defmy_decorator(f):
return5
任何函數都可以用作修飾器。這個例子中,修飾器被傳入一個函數,然後返回一個不同的東西。它完全吃掉了輸入的函數,並且永遠返回5。
@my_decorator
defhello():
print('hello')
>>> hello()
Traceback (most recent call last):
File"", line1,in
TypeError:'int'object isnotcallable
'int'object isnotcallable
由於修飾器返回的是INT,INT不是Callable,因此不能作為函數調用。別忘了,修飾器的返回值會替換掉Hello。
>>> hello
5
絕大多數情況下,我們希望修飾器返回的對象能夠模擬被修飾的函數。這就是說,修飾器返回的對象應該也是個函數。
例如,假設我們希望在每次函數被調用時輸出一行文字,我們可以寫個函數輸出資訊,然後再調用輸入的函數。但這個函數必須由修飾器返回。因此我們的函數得寫成嵌套的形式,如:
defmydecorator(f):# f is the function passed to us from python
deflog_f_as_called():
print(f'was called.')
f()
returnlog_f_as_called
從上面的代碼可以看出,我們定義了個嵌套的函數,該嵌套函數被修飾器返回。這樣,Hello函數依然可以被當做函數調用,調用者並不知道Hello被修飾過了。現在Hello函數可以這樣定義:
@mydecorator
defhello():
print('hello')
輸出如下:
>>> hello()
was called.
hello
(注意:中的數字代表記憶體地址,因此每個人的都會不一樣。)
正確包裝函數
如果有必要,函數可以被修飾多次。這種情況下,修飾器會引起連鎖反應。本質上,每個修飾器的返回值都會傳遞給上一層的修飾器,直到最頂層。例如,下面的代碼:
@a
@b
@c
defhello():
print('hello')
解釋器實際上執行的是hello = a(b(c(hello))),所有修飾器都會互相包裝。你可以用之前定義的修飾器測試這一點,使用兩次就好:
@mydecorator
@mydecorator
defhello():
print('hello')
>>> hello()
.a at0x7f277383d378> was called.
was called.
hello
你會注意到,第一個修飾器包裝了第二個,然後產生了不同的輸出。
有意思的是,第一行輸出的結果是.a at 0x7f277383d378>,而不是像第二行那樣輸出我們期待的資訊:。
這是因為修飾器返回的是個新函數,這個新函數不叫Hello。作為例子來說這無所謂,但實際上這可能會讓測試失敗,或者讓試圖自省函數屬性的過程失敗。
所以,如果修飾器的思想是模擬被修飾的函數的行為,那麽它也應該模擬被修飾函數的樣子。幸運的是,有個Python標準庫functools模塊提供的修飾器wraps能做到這一點:
importfunctools
defmydecorator(f):
@functools.wraps(f)# we tell wraps that the function we are wrapping is f
deflog_f_as_called():
print(f'was called.')
f()
returnlog_f_as_called
@mydecorator
@mydecorator
defhello():
print('hello')
>>> hello()
was called.
was called.
hello
現在,新的函數看起來跟它修飾的函數一模一樣。但是,我們這個修飾器依然只能修飾不返回任何值,並且不接受任何輸入的函數。如果想讓它更通用,就必須負責傳遞函數參數,並且返回同樣的值。可以這樣修改:
importfunctools
defmydecorator(f):
@functools.wraps(f)# wraps is a decorator that tells our function to act like f
deflog_f_as_called(*args, **kwargs):
print(f'was called with arguments=and kwargs=')
value = f(*args, **kwargs)
print(f'return value')
returnvalue
returnlog_f_as_called
現在每次調用都會產生輸出,包含函數接收到的所有輸入,以及函數的返回值。現在可以用它來修飾任意函數,獲得關於函數的輸入和輸出的調試資訊,而用不著手動編寫日誌代碼了。
給修飾器增加變量
如果你寫的修飾器不是隻給自己用,而是想在產品代碼裡使用,那你可能需要把所有print語句換成日誌輸出語句。那樣的話就需要定義日誌的級別。
都定義成DEBUG級別也許沒問題,但還是能根據函數選擇級別最好。我們可以給修飾器提供變量,以改變修飾器的行為。例如:
@debug(level='info')
defhello():
print('hello')
上面的代碼可以指定,被修飾的函數應該以info級別輸出日誌,而不是DEBUG級別。這個功能的實現方法是寫個函數,返回修飾器。
沒錯,修飾器也是個函數。所以這段代碼實質上是hello = debug('info')(hello)。兩對括號看起來很奇怪,不過本質上說,DEBUG是個返回函數的函數。因此,修改我們之前的修飾器,我們還需要一層嵌套,這樣代碼如下所示:
import functools
defdebug(level):
defmydecorator(f)
@functools.wraps(f)
deflog_f_as_called(*args, **kwargs):
logger.log(level, f' was called with arguments= and kwargs=')
value = f(*args, **kwargs)
logger.log(level, f' return value ')
returnvalue
returnlog_f_as_called
returnmydecorator
上面的修改將DEBUG變成了一個返回修飾器的函數,返回的修飾器會使用正確的日誌級別。這段代碼看起來不太好看,而且嵌套太多了。
有個很酷的小技巧我非常喜歡,就是給DEBUG添加默認的level參數,返回一個部分函數。部分函數是“不完整的函數調用”,它包含一個函數和一些參數,這樣部分函數可以作為一個整體來傳遞,而無需調用實際的函數。
importfunctools
defdebug(f=None, *, level='debug'):
iffisNone:
returnfunctools.partial(debug, level=level)
@functools.wraps(f)# we tell wraps that the function we are wrapping is f
deflog_f_as_called(*args, **kwargs):
logger.log(level,f'was called with arguments=and kwargs=')
value = f(*args, **kwargs)
logger.log(level,f'return value')
returnvalue
returnlog_f_as_called
現在修飾器可以正常工作了:
@debug
defhello():
print('hello')
這樣就會使用DEBUG級別。或者可以覆蓋log級別:
@debug('warning')
defhello():
print('hello')
原文:https://timber.io/blog/decorators-in-python/
作者:Nick Humrich
譯者:彎月,責編:胡巍巍