7.1 创建装饰器
装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。很可能你已经使用过装饰器作为自己的包装函数。最简单的装饰器可能就是本体函数(identity function),它除了返回原函数什么都不做。
def identity(f): return f
然后就可以像下面这样使用这个装饰器:
@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是基于位置的参数还是关键字参数,而只需在字典中查找即可。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论