11.decorator裝飾器
動態的函數增加功能
方法一:給每個函數添加printlog
方法二:通過高階函數返回新函數
方法三:內置@語法
12.編寫無參數decorator
Python的 decorator 本質上就是一個高階函數,它接收一個函數作爲參數,然後,返回一個新函數。
使用 decorator 用Python提供的 @ 語法,這樣可以避免手動編寫f = decorate(f) 這樣的代碼。
考察一個@log的定義:
def log(f):
def fn(x):
print 'call ' + f.__name__ + '()...'
return f(x)
return fn
對於階乘函數,@log工作得很好:
@log
deffactorial(n):
return reduce(lambda x,y: x*y, range(1,n+1))
printfactorial(10)
結果:
callfactorial()...
3628800
但是,對於參數不是一個的函數,調用將報錯:
@log
def add(x, y):
return x + y
print add(1, 2)
結果:
Traceback (mostrecent call last):
File "test.py", line 15, in<module>
print add(1,2)
TypeError: fn()takes exactly 1 argument (2 given)
因爲 add() 函數需要傳入兩個參數,但是 @log 寫死了只含一個參數的返回函數。
要讓 @log 自適應任何參數定義的函數,可以利用Python的 *args 和 **kw,保證任意個數的參數總是能正常調用:
def log(f):
def fn(*args, **kw):
print 'call ' + f.__name__ + '()...'
return f(*args, **kw)
return fn
現在,對於任意函數,@log 都能正常工作。
任務
請編寫一個@performance,它可以打印出函數調用的時間。
13.編寫帶參數decorator
考察上一節的 @log 裝飾器:
def log(f):
def fn(x):
print 'call ' + f.__name__ + '()...'
return f(x)
return fn
發現對於被裝飾的函數,log打印的語句是不能變的(除了函數名)。
如果有的函數非常重要,希望打印出'[INFO] call xxx()...',有的函數不太重要,希望打印出'[DEBUG] call xxx()...',這時,log函數本身就需要傳入'INFO'或'DEBUG'這樣的參數,類似這樣:
@log('DEBUG')
def my_func():
pass
把上面的定義翻譯成高階函數的調用,就是:
my_func =log('DEBUG')(my_func)
上面的語句看上去還是比較繞,再展開一下:
log_decorator =log('DEBUG')
my_func =log_decorator(my_func)
上面的語句又相當於:
log_decorator =log('DEBUG')
@log_decorator
def my_func():
pass
所以,帶參數的log函數首先返回一個decorator函數,再讓這個decorator函數接收my_func並返回新函數:
deflog(prefix):
def log_decorator(f):
def wrapper(*args, **kw):
print '[%s] %s()...' % (prefix,f.__name__)
return f(*args, **kw)
return wrapper
return log_decorator
@log('DEBUG')
def test():
pass
print test()
執行結果:
[DEBUG]test()...
None
對於這種3層嵌套的decorator定義,你可以先把它拆開:
# 標準decorator:
deflog_decorator(f):
def wrapper(*args, **kw):
print '[%s] %s()...' % (prefix,f.__name__)
return f(*args, **kw)
return wrapper
returnlog_decorator
# 返回decorator:
deflog(prefix):
return log_decorator(f)
拆開以後會發現,調用會失敗,因爲在3層嵌套的decorator定義中,最內層的wrapper引用了最外層的參數prefix,所以,把一個閉包拆成普通的函數調用會比較困難。不支持閉包的編程語言要實現同樣的功能就需要更多的代碼。
任務
上一節的@performance只能打印秒,請給 @performace 增加一個參數,允許傳入's'或'ms':
@performance('ms')
deffactorial(n):
return reduce(lambda x,y: x*y, range(1,n+1))
14.完善decorator
@decorator可以動態實現函數功能的增加,但是,經過@decorator“改造”後的函數,和原函數相比,除了功能多一點外,有沒有其它不同的地方?
在沒有decorator的情況下,打印函數名:
def f1(x):
pass
printf1.__name__
輸出: f1
有decorator的情況下,再打印函數名:
def log(f):
def wrapper(*args, **kw):
print 'call...'
return f(*args, **kw)
return wrapper
@log
def f2(x):
pass
printf2.__name__
輸出: wrapper
可見,由於decorator返回的新函數函數名已經不是'f2',而是@log內部定義的'wrapper'。這對於那些依賴函數名的代碼就會失效。decorator還改變了函數的__doc__等其它屬性。如果要讓調用者看不出一個函數經過了@decorator的“改造”,就需要把原函數的一些屬性複製到新函數中:
def log(f):
def wrapper(*args, **kw):
print 'call...'
return f(*args, **kw)
wrapper.__name__ = f.__name__
wrapper.__doc__ = f.__doc__
return wrapper
這樣寫decorator很不方便,因爲我們也很難把原函數的所有必要屬性都一個一個複製到新函數上,所以Python內置的functools可以用來自動化完成這個“複製”的任務:
importfunctools
def log(f):
@functools.wraps(f)
def wrapper(*args, **kw):
print 'call...'
return f(*args, **kw)
return wrapper
最後需要指出,由於我們把原函數簽名改成了(*args, **kw),因此,無法獲得原函數的原始參數信息。即便我們採用固定參數來裝飾只有一個參數的函數:
def log(f):
@functools.wraps(f)
def wrapper(x):
print 'call...'
return f(x)
return wrapper
也可能改變原函數的參數名,因爲新函數的參數名始終是 'x',原函數定義的參數名不一定叫 'x'。
任務
請思考帶參數的@decorator,@functools.wraps應該放置在哪:
defperformance(unit):
def perf_decorator(f):
def wrapper(*args, **kw):
???
return wrapper
return perf_decorator
注意@functools.wraps應該作用在返回的新函數上