想理解Python的decorator首先要知道在Python中函數也是一個對象,所以你可以
- 將函數復制給變量
- 將函數當做參數
- 返回一個函數
函數在Python中給變量的用法一樣也是一等公民,也就是高階函數(High Order Function)。所有的魔法都是由此而來。
1,起源
我們想在函數login中輸出調試信息,我們可以這樣做
def login(): print('in login') def printdebug(func): print('enter the login') func() print('exit the login') printdebug(login)
這個方法討厭的是每次調用login是,都通過printdebug來調用,但畢竟這是可行的。
2,讓代碼變得優美一點
既然函數可以作為返回值,可以賦值給變量,我們可以讓代碼優美一點。
def login(): print('in login') def printdebug(func): def __decorator(): print('enter the login') func() print('exit the login') return __decorator #function as return value debug_login = printdebug(login) #function assign to variable debug_login() #execute the returned function
這樣我們每次只要調用debug_login就可以了,這個名字更符合直覺。我們將原先的兩個函數printdebug和login綁定到一起,成為debug_login。這種耦合叫內聚:-)。
3,讓代碼再優美一點
printdebug和login是通過debug_login = printdebug(login)這一句來結合的,這一句似乎也是多余的,能不能在定義login是加個標注,從而將printdebug和login結合起來?
上面的代碼從語句組織的角度來講很難再優美了,Python的解決方案是提供一個語法糖(Syntax Sugar),用一個@符號來結合它們。
def printdebug(func): def __decorator(): print('enter the login') func() print('exit the login') return __decorator @printdebug #combine the printdebug and login def login(): print('in login') login() #make the calling point more intuitive
可以看出decorator就是一個:使用函數作參數並且返回函數的函數。通過改進我們可以得到:
- 更簡短的代碼,將結合點放在函數定義時
- 不改變原函數的函數名
在Python解釋器發現login調用時,他會將login轉換為printdebug(login)()。也就是說真正執行的是__decorator這個函數。
4,加上參數
1,login函數帶參數
login函數可能有參數,比如login的時候傳人user的信息。也就是說,我們要這樣調用login:
login(user)
Python會將login的參數直接傳給__decorator這個函數。我們可以直接在__decorator中使用user變量:
def printdebug(func): def __decorator(user): #add parameter receive the user information print('enter the login') func(user) #pass user to login print('exit the login') return __decorator @printdebug def login(user): print('in login:' + user) login('jatsz') #arguments:jatsz
我們來解釋一下login(‘jatsz’)的調用過程:
[decorated] login(‘jatsz’) => printdebug(login)(‘jatsz’) => __decorator(‘jatsz’) => [real] login(‘jatsz’)
2,裝飾器本身有參數
我們在定義decorator時,也可以帶入參數,比如我們這樣使用decorator,我們傳入一個參數來指定debug level。
@printdebug(level=5) def login pass
為了給接收decorator傳來的參數,我們在原本的decorator上在包裝一個函數來接收參數:
def printdebug_level(level): #add wrapper to recevie decorator's parameter def printdebug(func): def __decorator(user): print('enter the login, and debug level is: ' + str(level)) #print debug level func(user) print('exit the login') return __decorator return printdebug #return original decorator @printdebug_level(level=5) #decorator's parameter, debug level set to 5 def login(user): print('in login:' + user) login('jatsz')
我們再來解釋一下login(‘jatsz’)整個調用過程:
[decorated]login(‘jatsz’) => printdebug_level(5) => printdebug[with closure value 5](login)(‘jatsz’) => __decorator(‘jatsz’)[use value 5] => [real]login(‘jatsz’)
5,裝飾有返回值的函數
有時候login會有返回值,比如返回message來表明login是否成功。
login_result = login(‘jatsz’)
我們需要將返回值在decorator和調用函數間傳遞:
def printdebug(func): def __decorator(user): print('enter the login') result = func(user) #recevie the native function call result print('exit the login') return result #return to caller return __decorator @printdebug def login(user): print('in login:' + user) msg = "success" if user == "jatsz" else "fail" return msg #login with a return value result1 = login('jatsz'); print result1 #print login result result2 = login('candy'); print result2
我們解釋一下返回值的傳遞過程:
...omit for brief…[real][msg from login(‘jatsz’) => [result from]__decorator => [assign to] result1
6,應用多個裝飾器
我們可以對一個函數應用多個裝飾器,這時我們需要留心的是應用裝飾器的順序對結果會產生。影響比如:
def printdebug(func): def __decorator(): print('enter the login') func() print('exit the login') return __decorator def others(func): #define a other decorator def __decorator(): print '***other decorator***' func() return __decorator @others #apply two of decorator @printdebug def login(): print('in login:') @printdebug #switch decorator order @others def logout(): print('in logout:') login() print('---------------------------') logout()
我們定義了另一個裝飾器others,然后我們對login函數和logout函數分別應用這兩個裝飾器。應用方式很簡單,在函數定義是直接用兩個@@就可以了。我們看一下上面代碼的輸出:
$ python deoc.py ***other decorator*** enter the login in login: exit the login --------------------------- enter the login ***other decorator*** in logout: exit the login
我們看到兩個裝飾器都已經成功應用上去了,不過輸出卻不相同。造成這個輸出不同的原因是我們應用裝飾器的順序不同。回頭看看我們login的定義,我們是先應用others,然后才是printdebug。而logout函數真好相反,發生了什么?如果你仔細看logout函數的輸出結果,可以看到裝飾器的遞歸。從輸出可以看出:logout函數先應用printdebug,打印出“enter the login”。printdebug的__decorator調用中間應用了others的__decorator,打印出“***other decorator***”。其實在邏輯上我們可以將logout函數應用裝飾器的過程這樣看(偽代碼):
@printdebug #switch decorator order ( @others ( def logout(): print('in logout:') ) )
我們解釋一下整個遞歸應用decorator的過程:
[printdebug decorated]logout() =>
printdebug.__decorator[call [others decorated]logout() ] =>
printdebug.__decorator.other.__decorator[call real logout]
7,靈活運用
什么情況下裝飾器不適用?裝飾器不能對函數的一部分應用,只能作用於整個函數。
login函數是一個整體,當我們想對部分函數應用裝飾器時,裝飾器變的無從下手。比如我們想對下面這行語句應用裝飾器:
msg = "success" if user == "jatsz" else "fail"
怎么辦?
一個變通的辦法是“提取函數”,我們將這行語句提取成函數,然后對提取出來的函數應用裝飾器:
def printdebug(func): def __decorator(user): print('enter the login') result = func(user) print('exit the login') return result return __decorator def login(user): print('in login:' + user) msg = validate(user) #exact to a method return msg @printdebug #apply the decorator for exacted method def validate(user): msg = "success" if user == "jatsz" else "fail" return msg result1 = login('jatsz'); print result1
來個更加真實的應用,有時候validate是個耗時的過程。為了提高應用的性能,我們會將validate的結果cache一段時間(30 seconds),借助decorator和上面的方法,我們可以這樣實現:
import time dictcache = {} def cache(func): def __decorator(user): now = time.time() if (user in dictcache): result,cache_time = dictcache[user] if (now - cache_time) > 30: #cache expired result = func(user) dictcache[user] = (result, now) #cache the result by user else: print('cache hits') else: result = func(user) dictcache[user] = (result, now) return result return __decorator def login(user): print('in login:' + user) msg = validate(user) return msg @cache #apply the cache for this slow validation def validate(user): time.sleep(5) #simulate 10 second block msg = "success" if user == "jatsz" else "fail" return msg result1 = login('jatsz'); print result1 result2 = login('jatsz'); print result2 #this login will return immediately by hit the cache result3 = login('candy'); print result3
Reference:
http://stackoverflow.com/questions/739654/understanding-python-decorators --Understanding Python decorators
http://www.cnblogs.com/rhcad/archive/2011/12/21/2295507.html --Python裝飾器學習(九步入門)
http://www.python.org/dev/peps/pep-0318/ --PEP 318 -- Decorators for Functions and Methods