关闭

Python装饰器----应用示例(一)

标签: python装饰器函数注册用户认证格式化输出
128人阅读 评论(0) 收藏 举报
分类:

写装饰器

装饰器只不过是一种函数,接收被装饰的可调用对象作为它的唯一参数,然后返回一个可调用对象(就像前面的简单例子)
注意重要的一点,当装饰器被应用到被装饰函数上时,装饰器代码本身就会运行,而不是当被装饰函数被调用时.理解这个很关键,接下来的几个例子的讲解过程也会变得很清楚

第一个例子: 函数注册

看下面简单的函数注册:

registry = []
def register(decorated):
  registry.append(decorated)
  return decorated

注册器方法是一个简单的装饰器。它追加位置参数,也就是被装饰函数到registry变量中,然后不做改变地返回被装饰方法。任何接受register装饰器的方法会把它自己追加到registry变量上。

@register
def foo():
  return 3
@register
def bar():
  return 5

如果你访问了registry,可以很容易地在上面迭代并执行里面的函数。

answers = []
for func in registry:
   answers.append(func())

answers 列表现在回包含 [3, 5]. 这是因为函数已按次序执行,并且它们的返回值被追加到 answers中.
对于现有的函数注册,有几类简单的应用,例如添加“钩子(hooks)”到代码中,这样的话自定义的功能在条件事件之前或之后运行。 下面的Registry类能够处理这种情况:

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方法让然像之前一样按同样方法工作。用一个绑定(bound)的方法作为装饰器完全没问题。它接收self作为第一参数(像任何绑定方法一样),并且需要一个额外的位置参数,那就是被装饰函数,通过创建几个不同的 registry实例,你可以拥有一些完全分开的注册器。使用相同函数并且,用超过一个注册器注册它也是可行的,像下面展示的一样 :

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 baz(x=7):
  return x

运行两个注册器的run_alll方法,得到如下结果:

a.run_all() # [3, 7]
b.run_all() # [5, 7]

注意,run_all 方法能够使用参数,当它们运行时会把参数传给内部函数

a.run_all(x=4) # [4, 4]

运行时包装代码

以上这些装饰器都很简单,因为被装饰方法被传递后未经更改。然而,有些时候当被装饰方法执行时,你想要运行额外的功能。你通过返回一个添加了相关功能并且在它执行过程中调用被装饰方法的不同的可调用对象来实现。

简单的类型检查

这有一个简单的装饰器,确保函数接收到的每一个参数都是整数,否则进行报告:

def requires_ints(decorated):
  def inner(*args, **kwargs):
    #获取任何可能被发送的关键值参数
    kwarg_values = [i for i in kwargs.values()]
    #在发送给被装饰方法的每个值上面进行迭代,确保每一个都是整数;
    #如果不是抛 TypeError 
    for arg in list(args) + kwarg_values:
      if not isinstance(arg, int):
        raise TypeError('%s only accepts integers as   
             arguments.' % decorated.__name__)                
       #运行被装饰方法,返回结果
      return decorated(*args, **kwargs)
  return inner

发生了什么?
装饰器是 requires_ints. 它接受一个参数,即被装饰的可调用对象。这个装饰器做的唯一事情是返回一个新的可调用对象,一个内部的本地函数。这个函数替代了被装饰的可调用对象。你可以看到它如何发挥作用,声明一个函数并且用requires_ints来装饰

@requires_ints
def foo(x, y):
"""Return the sum of x and y."""
  return x + y

注意如果你运行 help(foo)获取的:

Help on function inner in module __main__:
inner(*args, **kwargs)
(END)

inner 函数已被指派了名字foo,而不是初始的,已定义了的函数。如果你运行 foo(3, 5), inner 函数会用这些参数来运行,inner函数进行类型检查,然后运行被装饰函数,因为inner函数调用它,使用decorated(*args, **kwargs),返回8.没有这个调用,被装饰方法会被忽略。
保留helpPreserving the help
一般不想让装饰器破坏你的函数的docstring或者操纵help输出。
因为装饰器是用来添加通用的和可重用功能的工具,他们有必要更泛化些。
并且,通常来说如果有人使用一个函数试图在上面运行help,他想要的是关于函数内脏(guts)的信息,而不是外壳(shell)的信息。解决这个问题的方法实际上应用到了 … 仍然是装饰器. Python 实现了一个叫做 @functools.wraps 的装饰器,它复制一个函数的内部元素到另一个函数。它把一个函数的重要的内省元素(introspection
elements)复制给另一个函数。
这是同一个@requires_ints 装饰器, 但添加了@functools.wraps的使用:

import functools
def requires_ints(decorated):
  @functools.wraps(decorated)
  def inner(*args, **kwargs):
    #获取可能已作为键值参数发送的任何值
    kwarg_values = [i for i in kwargs.values()]
    #迭代发送给被装饰函数的每个值, 并
    #确保每个参数都是整数,否则抛TypeError
    for arg in args + kwarg_values:
      if not isinstance(i, int):
        raise TypeError('%s only accepts integers as 
         arguments.' %decorated.__name__)
    #运行被装饰函数然后返回结果
    return decorated(*args, **kwargs)
  return inner

装饰器本身几乎没有改变,除了第二行给inner函数使用了@functools.wraps装饰器。你现在必须导入functools(在标准库中)。你也会注意到些额外语法。这个装饰器实际上使用了一个参数(稍后会有更多)。

现在你可以应用这个装饰器给相同的函数,像下面这样:

@requires_ints
def foo(x, y):
"""Return the sum of x and y."""
  return x + y

现在当你运行help(foo)的结果:

Help on function foo in module __main__:
foo(x, y)
Return the sum of x and y.
(END)

你看到了 foo的docstring ,同时还有它的方法签名,然而在盖头(hood)下面,@requires_ints装饰器仍然被应用,并且 inner函数仍然正常运行 。取决于你使用的python版本,运行结果可能稍有不同,尤其当忽略函数签名时。前面的输出源自Python 3.4。然而在python 2,提供的函数签名仍然有点隐秘(因此,是*args和**kwargs而不是x和y)

用户认证

这个模式(即在运行被装饰方法前进行过滤验证)的通常使用场景是用户认证。考虑一个需要user作为它的第一个参数的方法,user应该是User和AnonymousUser类的实例:

class User(object):
"""A representation of a user in our application."""
  def __init__(self, username, email):
    self.username = username
    self.email = email
class AnonymousUser(User):
"""An anonymous user; a stand-in for an actual user that nonetheless
is not an actual 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):
    """Verify that the user is truthy; if so, run the  
    decorated method,
    and if not, raise ValueError.
    """
    # Ensure that user is truthy, and of the correct type.
    # The "truthy"check will fail on anonymous users, since the
    # AnonymousUser subclass has a ‘__nonzero__‘ method that
    # returns False.
    if user and isinstance(user, User):
      return func(user, *args, **kwargs)
    else:
      raise ValueError('A valid user is required to run  
        this.')
  return inner

这个装饰器应用了一个通用的,需要样板化的验证—-用户是否登录进系统的验证。当你把它作为装饰器导入,它可重用且易于管理,它应用至函数上也清晰明了。注意这个装饰器只会正确地包装一个函数或者静态方法,如果包装一个类的绑定方法就会失败,这是因为装饰器忽视了发送self作为第一个参数到绑定方法的需要。

格式化输出

除了过滤一个函数的输入,装饰器的另一个用处是过滤一个函数的输出。当你用Python工作时,只要可能就希望使用Python本地对象。然而通常想要一个序列化的输出格式(例如,JSON)
在每个相关函数的结尾手动转换成JSON会显得很笨(也不是个好主意)。
理想的你应该使用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_nothing():
  return {'status': 'done'}

在Python shell中运行这个函数:

>>> do_nothing()
'{"status": "done"}'

结果是一个包含JSON的字符串,而不是一个字典。
这个装饰器的优美在于它的简洁。把这个装饰器应用到一个函数,本来返回python字典,列表或者其它对象的函数现在会返回它的JSON序列化的版本。你可能会问这有什么价值?毕竟你加了一行装饰器,实质上只移除了一行调用json.dumps的代码。
然而,由于应用的需求会扩展,还是考虑一下拥有此装饰器的价值。

例如,某种异常需要被捕获,并以特定的格式化的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):
  """Run the decorated function, serialize the result of   
  that function
  to JSON, and return the JSON string.
  """
  @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)
  return inner

通过使用错误处理增强@json_output装饰器,你已经把该功能添加给了应用了这个装饰器的任何函数。
这是让装饰器如此有价值的部分原因。对于代码轻便化,可重用化而言,它们是非常有用的工具。
现在,如果一个用@json_output装饰的函数抛出了JSONOutputError异常,就会有特别的错误处理:

@json_output
def error():
  raise JSONOutputError('This function is erratic.')

运行error 函数:

>>> error()
'{"status": "error", "message": "This function is erratic."}'

注意,只有JSONOutputError异常类(或它的子类)会获得这种特别的错误处理。任何其它异常会正常通过,并产生堆栈跟踪。

实质上,装饰器是避免重复你自己的工具,并且它们的部分价值在于给未来的维护提供钩子(hooks)。这些不用装饰器也可以实现,考虑要求用户登录进系统的例子,写一个函数并把它放在需要这项功能的函数的入口处就行了。装饰器首先是一种语法糖(syntactic sugar)。然而是一种很有价值的语法糖。毕竟,相较于写,代码更多时候用来读,而且你可以一眼定位到装饰器的位置。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:3869次
    • 积分:299
    • 等级:
    • 排名:千里之外
    • 原创:13篇
    • 转载:5篇
    • 译文:14篇
    • 评论:2条