预计阅读本页时间:-
7.1 创建装饰器
装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。很可能你已经使用过装饰器作为自己的包装函数。最简单的装饰器可能就是本体函数(identity function),它除了返回原函数什么都不做。
def identity(f):
return f
然后就可以像下面这样使用这个装饰器:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
@identity
def foo():
return 'bar'
它和下面的过程类似:
def foo():
return 'bar'
foo = identity(foo)
这个装饰器没什么用,但确实可以正常运行。只不过它什么都不做。
示例 7.1 注册装饰器
_functions = {}
def register(f):
global _functions
_functions[f.__name__] = f
return f
@register
def foo():
return 'bar'
在这个例子中,函数被注册并存储在一个字典里,以便后续可以根据函数名字提取函数。
在后面的几节中我会介绍Python中提供的标准装饰器,以及如何(何时)使用它们。
装饰器主要的应用场景是针对多个函数提供在其之前,之后或周围进行调用的通用代码。如果你写过Emacs Lisp代码,可能用过defadvice,它允许你定义围绕某个函数进行调用的代码。同样的东西还有开发人员已经用过的非常棒的方法组合,来源于CLOS(Common Lisp Object System)。
考虑这样一组函数,它们在被调用时需要对作为参数接收的用户名进行检查:
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)
显然,第一步就是要先分离出检查部分的代码:
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)
self.storage.put(food)
现在代码看上去稍微整洁了一点儿。但是有了装饰器能做得更好:
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(*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):
self.storage.put(food)
类似这样使用装饰器会让常用函数的管理更容易。如果有过正式的Python经验的话,这看起来有点儿老生常谈,但你可能没有意识到这种实现装饰器的原生方法有一些主要的缺点。
正如前面提到的,装饰器会用一个动态创建的新函数替换原来的。然而,新函数缺少很多原函数的属性,如docstring和名字。
>>> 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
...
>>> foobar.func_doc
'Do crazy stuff.'
>>> foobar.__name__
'foobar'
>>> @is_admin
... def foobar(username="someone"):
... """Do crazy stuff."""
... pass
...
>>> foobar.__doc__
>>> foobar.__name__
'wrapper'
幸好,Python内置的functools模块通过其update_wrapper函数解决了这个问题,它会复制这些属性给这个包装器本身。update_wrapper的源代码是自解释的,如示例7.2所示。
示例 7.2 Python 3.3 中functools.update_wrapper的源代码
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
wrapper.__wrapped__ = wrapped
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
如果用这个函数改写前面的示例,代码看起来会更简洁:
>>> def foobar(username="someone"):
... """Do crazy stuff."""
... pass
...
>>> foobar = functools.update_wrapper(is_admin, foobar)
>>> foobar.__name__
'foobar'
>>> foobar.__doc__
'Do crazy stuff.'
手工调用update_wrapper创建装饰器很不方便,所以functools提供了名为wraps的装饰器,如示例7.3所示。
示例 7.3 使用functools.wraps
import functools
def check_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(*args, **kwargs)
return wrapper
class Store(object):
@check_is_admin
def get_food(self, username, food):
return self.storage.get(food)
目前为止,在我们的示例中总是假设被装饰的函数会有一个名为username的关键字参数传入,但情况并非总是如此。考虑到这一点,最好是提供一个更加智能的装饰器,它能查看被装饰函数的参数并从中提取需要的参数。
为此,inspect模块允许提取函数的签名并对其进行操作,如示例7.4所示。
示例 7.4 使用inspect获取函数参数
import functools
import inspect
def check_is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
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
@check_is_admin
def get_food(username, type='chocolate'):
return type + " nom nom nom!"
承担主要工作的函数是inspect.getcallargs,它返回一个将参数名字和值作为键值对的字典。在上面的例子中,这个函数返回{'username': 'admin', 'type': 'chocolate'}。这意味着我们的装饰器不必检查参数username是基于位置的参数还是关键字参数,而只需在字典中查找即可。