正文
一般來說,裝飾器是一個函式,接受一個函式(或者類)作為引數,返回值也是也是一個函式(或者類)。首先來看一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# -*- coding: utf-8 -*- def log_cost_time(func): def wrapped(*args, **kwargs): import time begin = time.time() try: return func(*args, **kwargs) finally: print 'func %s cost %s' % (func.__name__, time.time() - begin) return wrapped @log_cost_time def complex_func(num): ret = 0 for i in xrange(num): ret += i * i return ret #complex_func = log_cost_time(complex_func) if __name__ == '__main__': print complex_func(100000) code snippet 0 |
程式碼中,函式log_cost_time就是一個裝飾器,其作用也很簡單,列印被裝飾函式執行時間。
裝飾器的語法如下:
def func():pass
本質上等同於: func = dec(func)。
在上面的程式碼(code snippet 0)中,把line12註釋掉,然後把line18的註釋去掉,是一樣的效果。另外staticmethod和classmethod是兩個我們經常在程式碼中用到的裝飾器,如果對pyc反編譯,得到的程式碼一般也都是 func = staticmthod(func)這種模式。當然,@符號的形式更受歡迎些,至少可以少拼寫一次函式名。
裝飾器是可以巢狀的,如
@dec0
@dec1
def func():pass
等將於 func = dec0(dec1(fun))。
裝飾器也有“副作用“”,對於被log_cost_time裝飾的complex_calc, 我們檢視一下complex_func.__name__,輸出是:”wrapped“”。額,這個是log_cost_time裡面inner function(wrapped)的名字,呼叫者當然希望輸出是”complex_func”,為了解決這個問題,python提供了兩個函式。
- functools.update_wrapper
原型: functools.
update_wrapper
(wrapper, wrapped[, assigned][, updated])
第三個引數,將wrapped的值直接複製給wrapper,預設為(__doc__, __name__, __module__)
第四個引數,update,預設為(__dict__)
- functools.wraps: update_wrapper的封裝
-
This is a convenience function for invoking
partial(update_wrapper,wrapped=wrapped,assigned=assigned,updated=updated)
as a function decorator when defining a wrapper function.
簡單改改程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
import functools def log_cost_time(func): @functools.wraps(func) def wrapped(*args, **kwargs): import time begin = time.time() try: return func(*args, **kwargs) finally: print 'func %s cost %s' % (func.__name__, time.time() - begin) return wrapped |
再檢視complex_func.__name__ 輸出就是 “complex_func”
裝飾器也是可以帶引數的。我們將上面的程式碼略微修改一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def log_cost_time(stream): def inner_dec(func): def wrapped(*args, **kwargs): import time begin = time.time() try: return func(*args, **kwargs) finally: stream.write('func %s cost %s \n' % (func.__name__, time.time() - begin)) return wrapped return inner_dec import sys @log_cost_time(sys.stdout) def complex_func(num): ret = 0 for i in xrange(num): ret += i * i return ret if __name__ == '__main__': print complex_func(100000) code snippet 1 |
log_cost_time函式也接受一個引數,該引數用來指定資訊的輸出流,對於帶引數的decorator
@dec(dec_args)
def func(*args, **kwargs):pass
等價於 func = dec(dec_args)(*args, **kwargs)。
裝飾器對類的修飾也是很簡單的,只不過平時用得不是很多。舉個例子,我們需要給修改類的__str__方法,程式碼很簡單。
1 2 3 4 5 6 7 8 9 10 11 |
def Haha(clz): clz.__str__ = lambda s: "Haha" return clz <a href="http://www.jobbole.com/members/cxh1527">@Haha</a> class Widget(object): ''' class Widget ''' if __name__ == '__main__': w = Widget() print w |
那什麼場景下有必要使用decorator呢,設計模式中有一個模式也叫裝飾器。我們先簡單回顧一下設計模式中的裝飾器模式,簡單的一句話概述
動態地為某個物件增加額外的責任
由於裝飾器模式僅從外部改變元件,因此元件無需對它的裝飾有任何瞭解;也就是說,這些裝飾對該元件是透明的。
下圖來自《設計模式Java手冊》或者GOF的《設計模式》
回到Python中來,用decorator語法實現裝飾器模式是很自然的,比如文中的示例程式碼,在不改變被裝飾物件的同時增加了記錄函式執行時間的額外功能。當然,由於Python語言的靈活性,decorator是可以修改被裝飾的物件的(比如裝飾類的例子)。decorator在python中用途非常廣泛,下面列舉幾個方面:
(1)修改被裝飾物件的屬性或者行為
(2)處理被函式物件執行的上下文,比如設定環境變數,加log之類
(3)處理重複的邏輯,比如有N個函式都可能跑出異常,但是我們不關心這些異常,只要不向呼叫者傳遞異常就行了,這個時候可以寫一個catchall的decorator,作用於所用可能跑出異常的函式
1 2 3 4 5 6 7 8 |
def catchall(func): @functools.wraps(func) def wrapped(*args, **kwargs): try: return func(*args, **kwargs) except: pass return wrapped |
(4)框架程式碼,如flask, bottle等等,讓使用者很方便就能使用框架,本質上也避免了重複程式碼。
decorator的奇妙應用往往超出相應,經常在各種原始碼中看到各種神奇的用法,酷殼這篇文章舉的例子也不錯。
參考
- pep 0318:https://www.python.org/dev/peps/pep-0318/#syntax-alternatives
- PYTHON修飾器的函數語言程式設計:http://coolshell.cn/articles/11265.html