装饰器
装饰器只不过是一种函数,接收被装饰的可调用对象作为它的唯一参数,然后返回一个可调用对象
第一个例子:函数注册
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