返回介绍

7.1 创建装饰器

发布于 2024-01-23 21:41:46 字数 5150 浏览 0 评论 0 收藏 0

装饰器本质上就是一个函数,这个函数接收其他函数作为参数,并将其以一个新的修改后的函数进行替换。很可能你已经使用过装饰器作为自己的包装函数。最简单的装饰器可能就是本体函数(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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文