深入理解Python 裝飾器(decorator)
返璞歸真, 看山還是山
剛看到Python裝飾器時, 覺得很神奇。簡單實驗下,發現也就那麼回事。但是慢慢的看到越來越多的裝飾器。很多時候又不瞭解到底是怎麼回事了。
最後還是決定好好研究下。
先看看一些例項, 然後再來分析下原理
假設我們有如下的基本函式
def do_something():
for i in range(1000000):
pass
print "play game"
do_something()
結果如下:
play game
需求1: 統計函式的執行時間
1. 不是裝飾器的裝飾器
import time
def decorator(fun):
start = time.time()
fun()
runtime = time.time()-start
print runtime
def do_something():
for i in range(1000000):
pass
print "play game"
decorator(do_something)
結果如下:
play game
0.0299999713898
這種實現看上去還可以,但是每次呼叫的是decorator,還要把函式作為一個引數傳入。這樣需要修改呼叫的地方,使用起來就不方便了。
2. 最簡單的裝飾器
import time
def decorator(fun):
def wrapper():
start = time.time()
fun()
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something():
for i in range(1000000):
pass
print "play game"
do_something()
結果如下:
play game
0.0329999923706
裝飾器是在函式定義時前面加@,然後跟裝飾器的實現函式。可以看出,現在只要直接呼叫do_something就可以了。呼叫的地方不要作任何修改。
3. 目標函式帶固定引數的裝飾器
import time
def decorator(fun):
def wrapper(name):
start = time.time()
fun(name)
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something(name):
for i in range(1000000):
pass
print "play game " + name
do_something("san guo sha")
結果如下:
play game san guo sha
0.039999961853
實現很簡單, 就是給wrapper函式參加相同的引數
4. 目標函式帶不固定引數的裝飾器
import time
def decorator(fun):
def wrapper(*args, **kwargs):
start = time.time()
fun(*args, **kwargs)
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something(name):
for i in range(1000000):
pass
print "play game " + name
@decorator
def do_something2(user, name):
for i in range(1000000):
pass
print user+" play game " + name
do_something("san guo sha")
do_something2("wang xiao er","san guo sha")
結果如下:
play game san guo sha
0.029000043869
wang xiao er play game san guo sha
0.0310001373291
需求2: 目標函式每次呼叫重複執行指定的次數
5. 讓裝飾器帶引數
import time
def decorator(max):
def _decorator(fun):
def wrapper(*args, **kwargs):
start = time.time()
for i in xrange(max):
fun(*args, **kwargs)
runtime = time.time()-start
print runtime
return wrapper
return _decorator
@decorator(2)
def do_something(name):
for i in range(1000000):
pass
print "play game " + name
do_something("san guo sha")
結果如下:
play game san guo sha
play game san guo sha
0.0600001811981
6. 原理
看了這麼多例項, 裝飾器的基本型別也基本上都有了。是不是清楚了呢?
如果還是不清楚,那就繼續看下面的內容。
1 不帶引數的裝飾器
@a_decorator
def f(...):
...
#經過a_decorator後, 函式f就相當於以f為引數呼叫a_decorator返回結果。
f = a_decorator(f)
來分析這個式子, 可以看出至少要滿足以下幾個條件
1. 裝飾器函式執行在函式定義的時候
2. 裝飾器需要返回一個可執行的物件
3. 裝飾器返回的可執行物件要相容函式f的引數
2 驗證分析
1 裝飾器執行時間
import time
def decorator(fun):
print "decorator"
def wrapper():
print "wrapper"
start = time.time()
fun()
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something():
for i in range(1000000):
pass
print "play game"
結果如下:
decorator
可以看出, 這裡的do_something並沒有呼叫, 但是卻列印了decorator, 可wrapper沒有打印出來。也就是說decorator是在do_something呼叫的時候執行的。
2 返回可執行的物件
import time
def decorator(fun):
print "decorator"
def wrapper():
print "wrapper"
start = time.time()
fun()
runtime = time.time()-start
print runtime
return None
@decorator
def do_something():
for i in range(1000000):
pass
print "play game"
do_something()
結果如下:
decoratorTraceback (most recent call last):
File "deco.py", line 17, in <module>
do_something()
TypeError: 'NoneType' object is not callable
3 相容函式f的引數
import time
def decorator(fun):
print "decorator"
def wrapper():
print "wrapper"
start = time.time()
fun()
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something(name):
for i in range(1000000):
pass
print "play game"
do_something("san guo sha")
結果如下:
decoratorTraceback (most recent call last):
File "deco.py", line 17, in <module>
do_something("san guo sha")
TypeError: wrapper() takes no arguments (1 given)
看到這裡, 至少對不帶引數的裝飾器應該全弄清楚了, 也就是說能到看山還是山了。
3 帶引數的裝飾器
這裡就給一個式子, 剩下的問題可以自己去想
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
pass
#這個式子相當於
func = decomaker(argA, argB, ...)(func)
4 被裝飾過的函式的函式名
import time
def decorator(fun):
def wrapper():
start = time.time()
fun()
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something():
print "play game"
print do_something.__name__
結果如下:
wrapper
可以看出, do_something的函式名變成了wrapper,這不是我們想要的。原因估計各位也都清楚了。那要怎麼去解決呢?
import time
def decorator(fun):
def wrapper():
start = time.time()
fun()
runtime = time.time()-start
print runtime
wrapper.__name__ = fun.__name__
return wrapper
@decorator
def do_something():
print "play game"
print do_something.__name__
結果如下:
do_something
但是這個看起來是不是很不專業, python的unctools.wraps提供瞭解決方法
import time
import functools
def decorator(fun):
@functools.wraps(fun)
def wrapper():
start = time.time()
fun()
runtime = time.time()-start
print runtime
return wrapper
@decorator
def do_something():
print "play game"
print do_something.__name__
結果如下:
do_something
到此為止, 你是不是覺得已經完全明白了呢?
但事實是, 這其實還不夠
7. 裝飾器類
需求3: 讓函式只能執行指定的次數
前面我們講的都是函式式的裝飾器, 那麼類能不能成為裝飾器呢?
import time
import functools
class decorator(object):
def __init__(self, max):
self.max = max
self.count = 0
def __call__(self, fun):
self.fun = fun
return self.call_fun
def call_fun(self, *args, **kwargs):
self.count += 1
if ( self.count == self.max):
print "%s run more than %d times"%(self.fun.__name__, self.max)
elif (self.count<self.max):
self.fun(*args, **kwargs)
else:
pass
@decorator(10)
def do_something():
print "play game"
@decorator(15)
def do_something1():
print "play game 1"
for i in xrange(20):
do_something()
do_something1()
結果如下:
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
do_something run more than 10 times
play game 1
play game 1
play game 1
play game 1
play game 1
do_something1 run more than 15 times
是不是感覺有點怪, 但它確實是可行的。
在Python中, 其實函式也是物件。 反過來, 物件其實也可以像函式一樣呼叫, 只要在類的方法中實現__call__方法。回想一下建立物件的過程
class A:
def __init__(self):
pass
a = A()
這其實和函式呼叫沒什麼區別, 那麼把這個式子代入到之前兩個裝飾器的式子中,結果如下:
帶引數的裝飾器
fun = A.__init__(args)(fun)
不帶引數的裝飾器
fun = A.__init__(fun)()
現在裝飾器的內容基本差不多了。 還有一些問題, 可以自己去嘗試研究。
自己的才是自己的
還有幾個問題如下:
1. 類裝飾器(裝飾器裝飾的物件是類)
2. 類函式裝飾器(裝飾器裝飾的物件是類的函式)
3. 多個裝飾器一起使用(函式巢狀)