編寫傳參的裝飾器
通常我們見到的簡單裝飾器這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import json import functools def json_output(func): @functools.wraps(decorated) def inner(*args, **kwargs): result = func(*args, **kwargs) return json.dumps(result) return inner @json_output def f(): return {'status': 'done'} |
當裝飾器應用於函式 f
上時,它接受 f
作為其引數,返回一個函式 inner
,且將他繫結到變數f上。
示例中我們編寫的裝飾器 json_output
只接受一個隱式引數——即被裝飾的方法,在使用此裝飾器時本身看上去是並沒有引數的。然而有時候需要讓裝飾器自身帶有一些需要的資訊,從而使裝飾器可以使用恰當的方式裝飾方法。比如上面的例子中,我們想通過向裝飾器傳入不同的引數來控制輸出結果的縮排(indent)和排序(sort)。我們可以這麼做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import json import functools def json_output(indent=None, sort_keys=False): def actual_decorator(func): @functools.wraps(func) def inner(*args, **kwargs): result = func(*args, **kwargs) return json.dumps(result, indent=indent, sort_keys=sort_keys) return inner return actual_decorator @json_output(indent=4) def f(): return {'status': 'done'} |
理解傳參的裝飾器
初次看起來會覺得比較繞人,因為函式裡巢狀了兩個函式定義,然而實際上和之前一個版本的區別在於為了接收json序列化的引數多包裝了一層,所以
1 2 3 4 5 6 7 8 9 |
@json_output(indent=4) def f(): return {'status': 'done'} # 相當於 @actual_decorator def f(): return {'status': 'done'} |
這樣看起來就會明晰很多。
實際上, 裝飾器裡的 @
後接收一個函式,該函式以被裝飾的函式(例子中是f)為引數,並且返回一個函式。當需要在裝飾函式的同時傳入引數的話,那麼就需要多包裝一層,先傳入引數(例子中是 indent=4
)返回一個裝飾的函式(例子中是 actual_decorator
), 這個返回的的函式 就跟以前一樣接受被裝飾的函式(f)作為引數並且返回一個函式作為裝飾最後的方法供呼叫。
傳參和不傳參的相容
然而當我們像上面那樣定義裝飾器時,就不能這樣呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import json import functools def json_output(indent=None, sort_keys=False): def actual_decorator(func): @functools.wraps(func) def inner(*args, **kwargs): result = func(*args, **kwargs) return json.dumps(result, indent=indent, sort_keys=sort_keys) return inner return actual_decorator @json_output def f(): return {'status': 'done'} |
在實際的專案過程中,有時會出現這樣的狀況: 一開始寫的裝飾器時不需要使用時傳引數的,後來發現有必要傳引數,改好後原來不傳參的裝飾器不能正常使用了,這是修改原來使用的地方是項痛苦的事情。
這時候就需要對裝飾器做一個相容,使它在以下情況都可用:
1 2 3 4 |
@json_output @json_output() @json_output(indent=4) |
具體做法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import json import functools def json_output(decorated_=None, indent=None, sort_keys=False): if decorated_ and (indent or sort_keys): raise def actual_decorator(func): @functools.wraps(func) def inner(*args, **kwargs): result = func(*args, **kwargs) return json.dumps(result, indent=indent, sort_keys=sort_keys) return inner if decorated_: return actual_decorator(decorated_) else: return actual_decorator @json_output(indent=4) def f1(): return {'status': 'done'} @json_output def f2(): return {'status': 'done'} @json_output() def f3(): return {'status': 'done'} print f1() print f2() print f3() |
程式碼中關鍵的地方在於 json_output
在最後對引數 decorated
進行了判斷,有的話證明是不傳參呼叫,那麼直接返回 actual_decorator
的呼叫;沒有的話則代表是傳參型別的呼叫(雖然引數可能不存在),那麼返回 actual_decorator
。其中有點需要注意, josn_output
的傳參需要使用關鍵字引數,如果像下面這樣直接傳一個位置引數,那麼根據現在的實現會出現錯誤(因為它會被當成 decorated_
)。
1 2 3 4 |
@json_output(4) #錯誤的使用方法 def f4(): return {'status': 'done'} |
參考資料
- 《Python高階程式設計》 by Luke Sneeriger