python中使用装饰器
装饰器本质上是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。最简单的装饰器函数可能就是本体函数(identity function),它除了返回原函数什么都不做。
def identity(f):
return f
然后就可以像下面这样使用这个装饰器了:
@identity
def foo():
return 'bar'
它和下面的过程类似:
def foo():
return 'bar'
foo = identity(foo)
这个装饰器没什么用,但确实可以正常运行。只不过它什么也不做。
_functions = {}
def register(f):
global _functions
_functions[f.__name__] = f
return f
@register
def foo():
return 'bar'
在上面的例子中,函数被注册并存储在一个字典里,以便后续可以根据函数名字提取函数。
装饰器的主要应用场景:针对多个函数提供在其之前,之后或者周围进行调用的通用代码。
考虑这样一组函数,它们在被调用时需要对作为参数接收的用户名进行检查:
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 get food")
return self.storage.put(food)
显然,第一步就是要分离出检查部分的代码:
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)
现在代码看上去稍微整洁了一点。但有了装饰器能做的更好:
def check_is_admin(f):
def wrapper(*args, **kwargs):
if username != 'admin':
raise Exception("this user is not allowed to get food")
return f(*args, **kwargs)
return wrapper
class Store(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 self.storage.put(food)
类似这样使用装饰器,会让常用函数的管理更容易。如果有过正式的python经验的话,这有点儿老生常谈,但你可能没有意识到这种原生实现装饰器的方式有一些主要的缺陷。
正如前面提到的,装饰器会用一个动态创建的新函数来替换原有的。然而,新函数缺少很多原函数的属性,比如docstring和名字。
def foobar(username="someone"):
"""do crazy stuff"""
pass
foobar = functools.update_wrapper(is_admin, foobar)
print foobar.__name__ #输出结果:'foobar'
print foobar.__doc__ #输出结果:'do crazy stuff'
幸好,python内置的functools模块通过其update_wrapper函数解决了这个问题,它会复制这些属性给这个包装器本身。就像上面的例子中演示的一样。
手工调用update_wrapper创建装饰器很不方便,所以functools提供了名为wraps的装饰器,就像下面:
def check_is_admin(f):
@functools.wraps(f) #在此处使用functools.wraps
def wrapper(*args, **kwargs):
if username != 'admin':
raise Exception("this user is not allowed to get food")
return f(*args, **kwargs)
return wrapper
目前为止,在我们的示例中总是假设被装饰的函数会有一个名为username的关键字参数传入,但实际情况并非总是如此。考虑到这一点,最好提供一个更加智能的装饰器,它能查看被装饰函数的参数,并从中提取需要的参数。
def check_is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
# inspect.getcallargs将返回一个以参数名字和值作为键值对的字典。此时,装饰器函数不用关心username是基于位置的参数还是关键字参数
func_args = inspect.getcallargs(f, *args, **kwargs)
if func_args.get('username') != 'admin':
raise Exception("this user is not allowed to get food")
return f(*args, **kwargs)
return wrapper