裝飾器
裝飾器只不過是一種函數,接收被裝飾的可調用對象作爲它的唯一參數,然後返回一個可調用對象
第一個例子:函數註冊
registry = []
# 被裝飾的對象,是一個可調用對象
def register(decorated):
registry.append(decorated)
return decorated
註冊器方法是一個簡單的裝飾器,會把被裝飾函數添加到registry中,然後不做改變的返回裝飾方法
@register
def foo():
return 3
現在對上面的註冊函數的方法進行封裝
class Registry(object):
def __init__(self):
self._functions = []
def register(self, decorated):
self._functions.append(decorated)
return decorated
def run_all(self, *args, **kwargs):
return_values = []
for func in self._functions:
return_values.append(func(*args, **kwargs))
return return_values
通過上面的類,我們可以實現一些完全分開的註冊器,使用相同的實例方法 register,但是可以給不同的對象實現註冊器的功能,見下面
a = Registry()
b = Registry()
@a.register
def foo(x=3):
return x
@b.register
def bar(x=5):
return x
@a.register
@b.register
def boll(x=7):
return x
# 運行兩個註冊器的run_alll方法,得到如下結果:
a.run_all() # [3, 7]
b.run_all() # [5, 7]
接下來講解一下@functools.wraps(decorated)
正常情況下,被裝飾器裝飾後的函數,本身的_name_ 和 __doc__會發生變化,這個方法消除這些副作用的,它能保留原有函數的名稱和doc屬性
第二個例子 簡單的類型檢查
import functools
def requires_ints(decorated):
@functools.wraps(decorated)
def inner(*args, **kwargs):
kwargs_values = [for i in kwargs.values()]
params = args.extend(kwargs_values)
for param in params:
if not isinstance(param, int):
raise TypeError('%s only accepts integers as arguments.' %decorated.__name__)
return decorated(*args, **kwargs)
return inner
被裝飾的函數
@requires_ints
def foo(x, y):
return x + y
# 現在當你運行help(foo)的結果:
Help on function foo in module __main__:
foo(x, y)
Return the sum of x and y.
(END)
# 可以發現函數的__name__ 和 __doc__ 屬性並沒有發生變化
第三個例子 用戶認證
class User(object):
def __init__(self, username, email):
self.username = username
self.email = email
class AnonymousUser(User):
"""匿名用戶
"""
def __init__(self):
self.username = None
self.email = None
def __nonzero__(self):
return False
裝飾器在此成爲隔離用戶驗證的有力工具。@requires_user裝飾器可以很輕鬆地認證你獲得了一個User對象並且不是匿名user
import functools
def requires_user(func):
@functools.wraps(func):
def inner(user, *args, **kwargs):
if user and isinstance(user, User):
return func(*args, **kwargs)
else:
raise ValueError('A valid user is required to run this.')
return inner
第三個例子 格式化輸出
除了過濾一個函數的輸入,裝飾器的另一個用處是過濾一個函數的輸出。當你用Python工作時,只要可能就希望使用Python本地對象。然而通常想要一個序列化的輸出格式(例如,JSON)
import functools
import json
def json_output(decorated):
"""Run the decorated function, serialize the result of
that function to JSON, and return the JSON string.
"""
@functools.wraps(decorated)
def inner(*args, **kwargs):
result = decorated(*args, **kwargs)
return json.dumps(result)
return inner
給一個 簡單函數應用@json_output
裝飾器 :
@json_output
def do_noting():
return {'status': 'done'}
然而,由於應用的需求會擴展,還是考慮一下擁有此裝飾器的價值。
例如,某種異常需要被捕獲,並以特定的格式化json輸出,而不是讓異常上浮產生堆棧跟蹤,該怎麼做?因爲有裝飾器,這個功能很容易添加。
import functools
import json
class JsonOutputError(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
def json_output(decorated):
@functools.wraps(decorated)
def inner(*args, **kwargs):
try:
result = decorated(*args, **kwargs)
except JsonOutputError as ex:
result = {
'status': 'error',
'message': 'str(ex)'
}
return result
return inner
現在,如果一個用@json_output裝飾的函數拋出了JsonOutputError異常,就會有特別的錯誤處理:
@json_output
def error():
raise JsonOutputError('This function is erratic.')
運行error函數
>>> error()
'{"status": "error", "message": "This function is erratic."}'
第四個例子 日誌記錄Logging
import functools
import logging
import time
def logged(method):
@functools.wraps(method)
def inner(*args, **kwargs):
start = time.time()
return_value = method(*args, **kwargs)
end = time.time()
delta = end - start
logger = logging.getLogger('decorator.logged')
logger.warn('Called method %s at %.02f; execution time %.02f seconds; result %r.' %
(method.__name__, start, delta, return_value))
return return_value
return inner
帶參數的裝飾器
就是在不帶參數的裝飾器上面再加一層包裝,是這個函數變成一個裝飾器函數,主要用來返回一個裝飾器
import functools
import json
class JsonOutputError(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
def json_output(indent=None, sort_keys=None)
def actual_decorator(decorated):
@functools.wraps(decorated)
def inner(*args, **kwargs):
try:
result = decorated(*args, **kwargs)
except JsonOutputError as ex:
result = {
'status': 'error',
'message': 'str(ex)'
}
return json.dumps(result, indent=indent, sort_keys=sort_keys)
return inner
return actual_decorator
裝飾器中的那些坑
裝飾器可以讓你代碼更加優雅,減少重複,但也不全是優點,也會帶來一些問題。
1.位置錯誤的代碼
讓我們直接看實例代碼
def html_tags(tag_name):
print 'begin outer function.'
def wrapper_(func):
print "begin of inner wrapper function."
def wrapper(*args, **kwargs):
content = func(*args, **kwargs)
print "<{tag}>{content}</{tag}>".format(tag=tag_name, content=content)
print 'end of inner wrapper function.'
return wrapper
print 'end of outer function'
return wrapper_
@html_tags('b')
def hello(name='Toby'):
return 'Hello {}!'.format(name)
hello()
hello()
在裝飾器中我在各個可能的位置都加上了print語句,用於記錄被調用的情況。你知道他們最後打印出來的順序嗎?如果你心裏沒底,那麼最好不要在裝飾器函數之外添加邏輯功能,否則這個裝飾器就不受你控制了。以下是輸出結果:
begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello Toby!</b>
<b>Hello Toby!</b>
2.錯誤的函數簽名和文檔
裝飾器裝飾過的函數看上去名字沒變,其實已經變了。
def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
return wrapper
@logging
def say(something):
"""say something"""
print "say {}!".format(something)
print say.__name__ # wrapper
可以在wrapper上加上functools.wraps解決
3.不能裝飾@staticmethod 或者 @classmethod
當你想把裝飾器用在一個靜態方法或者類方法時,不好意思,報錯了。
class Car(object):
def __init__(self, model):
self.model = model
@logging # 裝飾實例方法,OK
def run(self):
print "{} is running!".format(self.model)
@logging # 裝飾靜態方法,Failed
@staticmethod
def check_model_for(obj):
if isinstance(obj, Car):
print "The model of your car is {}".format(obj.model)
else:
print "{} is not a car!".format(obj)
"""
Traceback (most recent call last):
...
File "example_4.py", line 10, in logging
@wraps(func)
File "C:\Python27\lib\functools.py", line 33, in update_wrapper
setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'staticmethod' object has no attribute '__module__'
"""
前面已經解釋了@staticmethod
這個裝飾器,其實它返回的並不是一個callable對象,而是一個staticmethod
對象,那麼它是不符合裝飾器要求的(比如傳入一個callable對象),你自然不能在它之上再加別的裝飾器。要解決這個問題很簡單,只要把你的裝飾器放在@staticmethod
之前就好了,因爲你的裝飾器返回的還是一個正常的函數,然後再加上一個@staticmethod
是不會出問題的。
class Car(object):
def __init__(self, model):
self.model = model
@staticmethod
@logging # 在@staticmethod之前裝飾,OK
def check_model_for(obj):
pass
如何優化你的裝飾器
decorator.py
decorator.py 是一個非常簡單的裝飾器加強包。你可以很直觀的先定義包裝函數wrapper()
,再使用decorate(func, wrapper)
方法就可以完成一個裝飾器。
from decorator import decorate
def wrapper(func, *args, **kwargs):
"""print log before a function."""
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
def logging(func):
return decorate(func, wrapper) # 用wrapper裝飾func
你也可以使用它自帶的@decorator
裝飾器來完成你的裝飾器。
from decorator import decorator
@decorator
def logging(func, *args, **kwargs):
print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
return func(*args, **kwargs)
decorator.py
實現的裝飾器能完整保留原函數的name
,doc
和args
,唯一有問題的就是inspect.getsource(func)
返回的還是裝飾器的源代碼,你需要改成inspect.getsource(func.__wrapped__)
。
wrapt
wrapt是一個功能非常完善的包,用於實現各種你想到或者你沒想到的裝飾器。使用wrapt實現的裝飾器你不需要擔心之前inspect中遇到的所有問題,因爲它都幫你處理了,甚至inspect.getsource(func)
也準確無誤。
import wrapt
# without argument in decorator
@wrapt.decorator
def logging(wrapped, instance, args, kwargs): # instance is must
print "[DEBUG]: enter {}()".format(wrapped.__name__)
return wrapped(*args, **kwargs)
@logging
def say(something): pass
使用wrapt你只需要定義一個裝飾器函數,但是函數簽名是固定的,必須是(wrapped, instance, args, kwargs)
,注意第二個參數instance
是必須的,就算你不用它。當裝飾器裝飾在不同位置時它將得到不同的值,比如裝飾在類實例方法時你可以拿到這個類實例。根據instance
的值你能夠更加靈活的調整你的裝飾器。另外,args
和kwargs
也是固定的,注意前面沒有星號。在裝飾器內部調用原函數時才帶星號。
如果你需要使用wrapt寫一個帶參數的裝飾器,可以這樣寫。
def logging(level):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
print "[{}]: enter {}()".format(level, wrapped.__name__)
return wrapped(*args, **kwargs)
return wrapper
@logging(level="INFO")
def do(work): pass