Python Decorator(裝飾器)
今天來說說 Python 裡的裝飾器 (decorator)。它不難,但卻幾乎是 “精通” Python 的路上的第一道關卡。讓我們來看看它到底是什麼東西,為什麼我們需要它。
#手寫裝飾器
現在我們要寫一個函式:
def add(x, y=10): return x + y |
然後我們想看看執行的結果,於是寫了幾個 print 語句:
print("add(10)", add(10))print("add(20, 30)", add(20, 30))print("add('a', 'b')", add('a', 'b'))# Results:# add(10) 20# add(20, 30) 50 |
現在我們想看看測試這個函式的效能,於是我們加上這個程式碼:
from time import timebefore = time()print("add(10)", add(10))after = time()print("time taken: {}".format(after - before))before = time()print("add(20, 30)", add(20, 30))after = time()print("time taken: {}".format(after - before))before = time()print("add('a', 'b')" |
程式碼馬上變得很複雜。但最重要的是,我們得寫一堆程式碼(複製貼上),程式設計師是懶惰的,所以我們就想到一些更簡單的方法,與其寫這麼多次,我們可以只寫一次程式碼:
from |
不論是程式碼的修改量還是程式碼的美觀程度,都比之前的版本要好!
但是,現在我們寫了另一個函式:
def sub(x, y=10): return x - y |
我們必須再為 sub
函式加上和 add
相同的效能測試程式碼:
def sub(x, y=10): before = time() result = x - y after = time() print('elapsed: ', after - before) return result |
作為一個懶惰的程式設計師,我們立馬就發現了,有一個 “模式” 反覆出現,即執行一個函式,並計算這個函式的執行時間。於是我們就可以把這個模式抽象出來,用函式:
from time import timedef timer(func, x, y = 10): before = time() result = func(x, y) after = time() print("elapsed: ", after - before) return resultdef add(x, y = 10): return x + ydef sub(x, y = 10): return x - yprint("add(10)", timer(add, 10))print("add(20, 30)", timer(add, 20, 30)) |
但這樣還是很麻煩,因為我們得改到所有的測試用例,把 add(20, 30)
改成
timer(add, 20, 30)
。於是我們進一步改進,讓 timer 返回函式:
def timer(func): def wraper(x, y=10): before = time() result = func(x, y) after = time() print("elapsed: ", after - before) return result return wraperdef add(x, y = 10): return x + yadd = timer(add)def sub(x, y = 10): return x - ysub = timer(sub)print("add(10)", add(10))print("add(20, 30)", add(20, 30)) |
這裡的最後一個問題是,我們的 timer 包裝的函式可能有不同的引數,於是我們可以進一步用 *args, **kwargs
來傳遞引數:
def timer(func): def wraper(*args, **kwargs): before = time() result = func(*args, **kwargs) after = time() print("elapsed: ", after - before) return result return wraper |
這裡的 timer
函式就是一個 “裝飾器”,它接受一個函式,並返回一個新的函式。在裝飾器的內部,對原函式進行了“包裝”。
#@ 語法糖
上一節是一個懶惰的程式設計師用原生的 Python 寫的裝飾器,但在裝飾器的使用上,用的是這個程式碼:
def add(x, y = 10): return x + yadd = timer(add) # <- notice thisdef sub(x, y = 10): return x - ysub = timer(sub) |
上面這個語句裡,我們把 add
的名字重複了 3 次,如果函式改了名字,我們就得改 3
處。懶惰的程式設計師就想了一個更“好”的方法,提供了一個語法來替換上面的內容:
def add(x, y=10): return x + y |
這就是我們最常見的裝飾器的形式了,這兩種寫法完全等價,只是 @
寫法更簡潔一些。
#帶引數的裝飾器
我們知道下面兩種程式碼是等價的:
def func(...): ...func = dec(func) |
我們可以把它當成是純文字的替換,於是可以是這樣的:
def func(...): ...func = dec(arg)(func) |
這也就是我們看到的“帶引數”的裝飾器。可見,只要 dec(arg)
的返回值滿足 “裝飾器” 的定義即可。(接受一個函式,並返回一個新的函式)
這裡舉一個例子(來源):
def use_logging(level): def decorator(func): def wrapper(*args, **kwargs): if level == "warn": logging.warn("%s is running" % func.__name__) elif level == "info": logging.info("%s is running" % func.__name__) return func(*args) return wrapper return decoratordef foo(name='foo'): print("i am %s" % name) |
先不管 use_logging
長什麼樣,先關心它的返回值 decorator
,看到 decorator
本身是一個函式,並且引數是函式,返回值是函式,於是確認 decorator
是一個 “裝飾器”。於是上面這種“帶引數的裝飾器”的作用也就很直接了。
#類作為裝飾器
如果說 Python 裡一切都是物件的話,那函式怎麼表示成物件呢?其實只需要一個類實現
__call__
方法即可。
class Timer: def __init__(self, func): self._func = func def __call__(self, *args, **kwargs): before = time() result = self._func(*args, **kwargs) after = time() print("elapsed: ", after - before) return resultdef add(x, y=10): return x + y |
也就是說把類的建構函式當成了一個裝飾器,它接受一個函式作為引數,並返回了一個物件,而由於物件實現了 __call__
方法,因此返回的物件相當於返回了一個函式。因此該類的建構函式就是一個裝飾器。
#小結
裝飾器中還有一些其它的話題,例如裝飾器中元資訊的丟失,如何在類及類的方法上使用裝飾器等。但本文裡我們主要目的是簡單介紹裝飾器的原因及一般的使用方法,能用上的地方就大膽地用上吧!