本文轉自http://mingxinglai.com/cn/2015/08/python-decorator/
[TOC]
1. 介紹
Python 2.2 開始提供了裝飾器(decorator),裝飾器作為修改函數的一種便捷方式,為程序員編寫程序提供了便利性和靈活性,適當使用裝飾器,能夠有效的提高代碼的可讀性和可維護性,然而,裝飾器并沒有被廣泛的使用,主要還是因為大多數人并不理解裝飾器的工作機制。
本文首先介紹了裝飾器的概念和用法(第2節),然后介紹了裝飾器使用過程中的注意事項(第3節),之后討論了裝飾器的使用場景和注意是想(第4節),最后提供了一些裝飾器的學習素材(第5節)。
2. 裝飾器
裝飾器本質上就是一個函數,這個函數接收其他函數作為參數,并將其以一個新的修改后的函數進行替換。概念比較抽象,一起來看兩個裝飾器的例子。
1. 裝飾器的概念
考慮這樣一組函數,它們在被調用時需要對某些參數進行檢查,在本例中,需要對用戶名進行檢查,以判斷用戶是否有相應的權限進行某些操作。
程序清單1
class Store(object):
def get_food(self, username, food):
if username != 'admin':
raise Exception("This user is not allowed to get food")
return self.storage.get(food)
def put_food(self, username, food):
if username != 'admin':
raise Exception("This user is not allowed to put food")
self.storage.put(food)
顯然,代碼有重復,作為一個有追求的工程師,我們嚴格遵守DRY(Don’t repeat yourself)原則,于是,代碼被改寫成了程序清單2這樣:
程序清單2
def check_is_admin(username):
if username != 'admin':
raise Exception("This user is not allowed to get food")
class Store(object):
def get_food(self, username, food):
check_is_admin(username)
return self.storage.get(food)
def put_food(self, username, food):
check_is_admin(username)
return self.storage.put(food)
現在代碼整潔一點了,但是,有裝飾器能夠做的更好:
程序清單3
def check_is_admin(f):
def wrapper(*args, **kwargs):
if kwargs.get('username') != 'admin':
raise Exception("This user is not allowed to get food")
return f(*arg, **kargs)
return wrapper
class Storage(object):
@check_is_admin
def get_food(self, username, food):
return self.storage.get(food)
@check_is_admin
def put_food(self, username, food):
return storage.put(food)
上面這段代碼就是使用裝飾器的典型例子:函數里面定義了一個函數,并將定義的這個函數作為返回值。這個例子足夠簡單,所以它的好處也不夠明顯,但是,卻可以很好的演示裝飾器的語法。
即使這樣,我們也可以說,程序清單3比程序清單2更好,因為程序清單3能夠將條件檢查與具體邏輯分隔開來。在本例中,check_is_admin只是預檢查,它的重要性不如具體的業務邏輯。我們將業務邏輯看做是這段程序的”重點”的話,那么,程序清單3一眼看過去就能看到”重點”,而程序清單2則不能,需要簡單的思考(轉個彎)才能區分條件檢查和業務邏輯。當然,你可能覺得這沒什么,但是,作為一名有追求的工程師,我們希望我們寫出的代碼能和散文一樣優美。
2. 裝飾器的本質
前面說過,裝飾器本質上就是一個函數,這個函數接收其他函數作為參數,并將其以一個新的修改后的函數進行替換。下面這個例子能夠更好地理解這句話。
程序清單4
def bread(func):
def wrapper():
print "</''''''\>"
func()
print "</______\>"
return wrapper
sandwich_copy = bread(sandwich)
sandwich_copy()
輸出結果如下:
</''''''\>
--ham--
</______\>
分析如下: bread是一個函數,它接受一個函數作為參數,然后返回一個新的函數,新的函數對原來的函數進行了一些修改和擴展,且這個新函數可以當做普通函數進行調用。
下面這段代碼和程序清單4輸出結果一摸一樣,只是用了python提供的裝飾器語法,看起來更加簡單直接。
程序清單5
def bread(func):
def wrapper():
print "</''''''\>"
func()
print "</______\>"
return wrapper
@bread
def sandwich(food="--ham--"):
print food
到這里,我們應該已經能夠理解裝飾器的作用和用法了,再強調一遍:裝飾器本質上就是一個函數,這個函數接收其他函數作為參數,并將其以一個新的修改后的函數進行替換
3. 使用裝飾器需要注意的地方
我們在上一節中演示了裝飾器的用法,可以看到,裝飾器其實很好理解,也非常簡單。但是,要用好裝飾器,還有一些我們需要注意的地方,這一節就對這些需要注意的地方進行了討論,首先討論了在使用裝飾器的情況下,如何保留原有函數的屬性(見3.1節);然后討論了如何實現一個更加智能的裝飾器;之后討論了使用多個裝飾器時,各個裝飾器的調用順序(見3.3節);最后說明了如何給裝飾器傳遞參數(見3.4節)。
1. 函數的屬性變化
裝飾器動態創建的新函數替換原來的函數,但是,新函數缺少很多原函數的屬性,如docstring和名字。
程序清單6
def is_admin(f):
def wrapper(*args, **kwargs):
if kwargs.get("username") != 'admin':
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return wrapper
def foobar(username='someone'):
"""Do crazy stuff"""
pass
@is_admin
def barfoo(username='someone'):
"""Do crazy stuff"""
pass
def main():
print foobar.func_doc
print foobar.__name__
print barfoo.func_doc
print barfoo.__name__
if __name__ == '__main__':
main()
程序清單6的輸出結果:
Do crazy stuff
foobar
None
wrapper
程序清單6中,我們定義了兩個函數foobar與barfoo,其中,barfoo使用裝飾器進行了封裝,我們獲取foobar與barfoo的docstring和函數名字,可以看到,使用了裝飾器的函數,不能夠正確獲取函數原有的docstring與名字,為了解決這個問題,可以使用python內置的 functools模塊。
程序清單7
import functools
def is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
if kwargs.get("username") != 'admin':
raise Exception("This user is not allowed to get food")
return f(*arg, **kwargs)
return wrapper
我們只需要增加一行代碼,就能夠正確地獲取函數的屬性。
此外,我們也可以向下面這樣:
def is_admin(f):
def wrapper(*args, **kwargs):
if kwargs.get("username") != 'admin':
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return functools.wraps(f)(wrapper) # important
當然,個人推薦第一種方法,因為第一種方法可讀性更強。
2. 使用inspect獲取函數參數
下面看一下程序清單8,它是否會正確輸出結果呢?
程序清單8
import functools
def check_is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print kwargs
if kwargs.get('username') != 'admin':
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return wrapper
@check_is_admin
def get_food(username, food='chocolate'):
return "{0} get food: {1}".format(username, food)
def main():
print get_food('admin')
if __name__ == '__main__':
main()
程序清單8會拋出一個異常,因為我們傳入的’admin’是一個位置參數,而我們卻去關鍵字參數(kwargs)獲取用戶名,因此,`kwargs.get(‘username’)返回None,那么,權限檢查發現,用戶沒有相應的權限,拋出異常。
為了提供一個更加智能的裝飾器,我們需要使用python的inspect模塊。inspect能夠取出函數的簽名,并對其進行操作,如下所示:
程序清單9
import functools
import inspect
def check_is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
func_args = inspect.getcallargs(f, *args, **kwargs)
print func_args
if func_args.get('username') != 'admin':
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return wrapper
@check_is_admin
def get_food(username, food='chocolate'):
return "{0} get food: {1}".format(username, food)
def main():
print get_food('admin')
if __name__ == '__main__':
main()
承擔主要工作的函數是inspect.getcallargs,它返回一個將參數名字和值作為鍵值對的字典,這程序清單7中,這個函數返回{'username':'admin','food':'chocolate'}
。這意味著我們的裝飾器不必檢查參數username是基于位置的參數還是基于關鍵字的參數,而只需在字典中查找即可。
3. 多個裝飾器的調用順序
多個裝飾器的調用順序也很好理解,我們一stackoverflow上的這個問題為例進行說明。
問題
How can I make two decorators in Python that would do the following?
@makebold
@makeitalic
def say():
return "Hello"
which should return
<b><i>Hello</i></b>
答案
def makebold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped
def makeitalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped
@makebold
@makeitalic
def hello():
return "hello world"
print hello() ## returns <b><i>hello world</i></b>
分析
我們在2.2節說過,裝飾器就是在外層進行了封裝:
@bread
sandwich()
sandwich_copy = bread(sandwich)
那么,封裝兩層可以像這樣理解:
@makebold
@makeitalic
hello()
hello-copy = makebold(makeitalic(helo))
因此,這樣理解以后,對于多個裝飾器的調用順序,就不再有疑問了。
4. 給裝飾器傳遞參數
下面通過一個官方的例子來看如何給裝飾器傳遞參數。官方介紹了一個非常有用的裝飾器,即設置超時器。如果函數調用超時,則拋出異常。
程序清單10
def timeout(seconds, error_message = 'Function call timed out'):
def decorated(func):
def _handle_timeout(signum, frame):
raise TimeoutError(error_message)
def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
return result
return functools.wraps(func)(wrapper)
return decorated
使用方法如下:
import time
@timeout(1, 'Function slow; aborted')
def slow_function():
time.sleep(5)
對應于我們這篇博客一直討論的例子,傳遞參數的代碼如下所示:
程序清單11
def is_admin(admin='admin'):
def decorated(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
if kwargs.get("username") != admin:
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return wrapper
return decorated
@is_admin(admin='root')
def barfoo(username='someone'):
"""Do crazy stuff"""
print '{0} get food'.format(username)
if __name__ == '__main__':
barfoo(username='root')
4. 裝飾器的使用場景與缺點
1. 裝飾器的使用場景
裝飾器雖然語法比較復雜,但是,在一些場景下,也確實比較有用。包括:
注入參數(提供默認參數,生成參數)
記錄函數行為(日志、緩存、計時什么的)
預處理/后處理(配置上下文什么的)
修改調用時的上下文(線程異步或者并行,類方法)
下面這個例子演示了上面提到的3中情況,如下所示:
程序清單12
def benchmark(func):
"""
A decorator that prints the time a function takes
to execute.
"""
import time
def wrapper(*args, **kwargs):
t = time.clock()
res = func(*args, **kwargs)
print func.__name__, time.clock()-t
return res
return wrapper
def logging(func):
"""
A decorator that logs the activity of the script.
(it actually just prints it, but it could be logging!)
"""
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print func.__name__, args, kwargs
return res
return wrapper
def counter(func):
"""
A decorator that counts and prints the number of times a function has been executed
"""
def wrapper(*args, **kwargs):
wrapper.count = wrapper.count + 1
res = func(*args, **kwargs)
print "{0} has been used: {1}x".format(func.__name__, wrapper.count)
return res
wrapper.count = 0
return wrapper
@counter
@benchmark
@logging
def reverse_string(string):
return str(reversed(string))
關于裝飾器的例子,官方列出了一個長長的列表,這里很多代碼可以直接拿來使用,如果需要詳細地了解裝飾器的使用場景,可以學習一下這份列表。
2. 裝飾器有哪些缺點
在我們目前的實際項目中,裝飾器使用還不夠多,所以沒有積累很多的經驗,下面是國外大神給出的裝飾器的缺點,以供參考:
Decorators were introduced in Python 2.4, so be sure your code will be run on >= 2.4.
Decorators slow down the function call. Keep that in mind.
You cannot un-decorate a function. (There are hacks to create decorators that can be removed, but nobody uses them.) So once a function is decorated, it’s decorated for all the code.
Decorators wrap functions, which can make them hard to debug.
5. 其他學習資料
本文較為全面地介紹了裝飾器的用法,也給出了裝飾器的使用場景和缺點。如果還需要進一步的學習裝飾器,可以了解一下下面這幾份資料:
python decorator library
source code of flask
Magic decorator syntax for asynchronous code in Python
A Python decorator that helps ensure that a Python Process is running only once