Python装饰器用法实例总结(新)

时间:2022-10-19 15:11:43

本文实例讲述了Python装饰器用法。分享给大家供大家参考,具体如下:

写装饰器

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

第一个例子: 函数注册

看下面简单的函数注册:

?
1
2
3
4
registry = []
def register(decorated):
 registry.append(decorated)
 return decorated

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

?
1
2
3
4
5
6
@register
def foo():
 return 3
@register
def bar():
 return 5

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

?
1
2
3
answers = []
for func in registry:
  answers.append(func())

answers 列表现在回包含 [3, 5]. 这是因为函数已按次序执行,并且它们的返回值被追加到 answers中.

对于现有的函数注册,有几类简单的应用,例如添加“钩子(hooks)”到代码中,这样的话自定义的功能在条件事件之前或之后运行。 下面的Registry类能够处理这种情况:

?
1
2
3
4
5
6
7
8
9
10
11
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实例,你可以拥有一些完全分开的注册器。使用相同函数并且,用超过一个注册器注册它也是可行的,像下面展示的一样 :

?
1
2
3
4
5
6
7
8
9
10
11
12
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方法,得到如下结果:

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

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

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

运行时包装代码

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

简单的类型检查

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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来装饰

?
1
2
3
4
@requires_ints
def foo(x, y):
"""Return the sum of x and y."""
 return x + y

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

?
1
2
3
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的使用:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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(在标准库中)。你也会注意到些额外语法。这个装饰器实际上使用了一个参数(稍后会有更多)。

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

?
1
2
3
4
@requires_ints
def foo(x, y):
"""Return the sum of x and y."""
 return x + y

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

?
1
2
3
4
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类的实例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

?
1
2
3
4
5
6
7
8
9
10
11
12
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 装饰器 :

?
1
2
3
@json_output
def do_nothing():
 return {'status': 'done'}

在Python shell中运行这个函数:

?
1
2
>>> do_nothing()
'{"status": "done"}'

结果是一个包含JSON的字符串,而不是一个字典。

这个装饰器的优美在于它的简洁。把这个装饰器应用到一个函数,本来返回python字典,列表或者其它对象的函数现在会返回它的JSON序列化的版本。你可能会问这有什么价值?毕竟你加了一行装饰器,实质上只移除了一行调用json.dumps的代码。

然而,由于应用的需求会扩展,还是考虑一下拥有此装饰器的价值。

例如,某种异常需要被捕获,并以特定的格式化的json输出,而不是让异常上浮产生堆栈跟踪,该怎么做?因为有装饰器,这个功能很容易添加。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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异常,就会有特别的错误处理:

?
1
2
3
@json_output
def error():
 raise JSONOutputError('This function is erratic.')

运行error 函数:

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

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

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

日志记录Logging

执行时包装代码的最后一个例子是一个通用的日志记录函数。 考虑下面引起函数调用的装饰器, 运行时间,
结果会被记录:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import functools
import logging
import time
def logged(method):
 """Cause the decorated method to be run and its results 
 logged, along
 with some other diagnostic information.
 """
 @functools.wraps(method)
 def inner(*args, **kwargs):
  #Record our start time.
  start = time.time()
  #Run the decorated method.
  return_value = method(*args, **kwargs)
  #Record our completion time, and calculate the delta.
  end = time.time()
  delta = end - start
  #Log the method call and the result.
  logger = logging.getLogger('decorator.logged')
  logger.warn('Called method %s at %.02f; execution time 
   %.02f seconds; result %r.' %
   (method.__name__, start, delta, return_value))   
   #Return the methods original return value.
  return return_value
 return inner

当应用到一个函数上后,这个装饰器正常地运行那个函数,但函数调用结束后会使用Python logging模块记录信息。

?
1
2
3
4
5
6
7
8
9
>>> import time
>>> @logged… def sleep_and_return(return_value):
... time.sleep(2)
... return return_value…
>>>
>>> sleep_and_return(42)
Called method sleep_and_return at 1424462194.70;
execution time 2.00 seconds; result 42.
42

不像先前的例子,这个装饰器不显式地更改函数调用. 不存在你应用这个装饰器后获得的结果与没有被装饰的函数的结果不一样的情况。这个装饰器做了些幕后工作,但并不改变实际结果。
值得注意的是, @json_output 和 @logged 装饰器都提供 inner 函数 ,这个函数简单地以最小的侦测采用和传递可变参数和关键字参数。

这是一种重要的模式。一种它尤其重要的方式是,许多装饰器可能被用来装饰纯粹的函数和类的方法。记住,在Python中,类中声明的方法会获得一个额外位置参数,即广为人知的self。当装饰器在使用时,它不会改变 (这就是为什么先前的requires_user装饰器在类的绑定方法上不起作用)

例如@json_result被用来装饰一个类的方法,inner函数被调用,它接收一个类的实例作为第一个参数。实际上这没有问题。在这种情况下,这个参数就是args[0],它被传送给被装饰方法。

装饰器参数

到目前为止列出的所有装饰器都没有任何参数。作为讨论过的内容,有一个暗含的参数–被装饰的方法。然而,有时让装饰器自身使用一些它需要的信息去装饰相关方法会有用处。

一个参数传给一个装饰器和一个参数传给一个正在调用的方法之间的不同是,当一个函数被声明并被装饰,传给装饰器的参数会被立刻处理。相反,传给函数的参数在函数调用时被处理。通过@functools.wraps的多次使用,你已经看到了一个参数传给装饰器的例子。它使用一个参数—-被包装的方法,方法的help和docstring等类似的东西应该被保留。然而,装饰器有内含的调用签名。他们使用一个位置参数–被装饰的方法。所以,这是怎么工作的?答案说来就复杂了。

回想运行时包装代码的基本装饰器 ,他们在局部范围声明了一个 inner 方法 然后返回它. 这就是由装饰器返回的可调用对象. 它被指派了被调用函数的名字. 使用参数的装饰器多添加一个包装层,这是因为,使用参数的装饰器不再是一个实际的装饰器。它是一个返回装饰器的函数,是一个使用一个参数(被装饰的方法)的函数。然后装饰函数并返回一个可调用对象。听起来混乱,考虑下面的例子,在这里,装饰器@json_output的功能被增强了,要求缩进和排序:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import json
class JSONOutputError(Exception):
 def __init__(self, message):
  self._message = message
 def __str__(self):
  return self._message
def json_output(indent=None, sort_keys=False):
 """Run the decorated function, serialize the result of
 that function
 to JSON, and return the JSON string.
 """
 def actual_decorator(decorated):
  @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, indent=indent,  
    sort_keys=sort_keys)
   return inner
 return actual_decorator

那么,发生了什么,为什么这回起作用?这是一个函数,json_output,接收两个参数(indent 和 sort_keys). 它返回另一个函数, 叫 actual_decorator, 这是 (如同名字表名的) 要作为装饰器使用的. 这是一个典型的装饰器—一个接收可调用对象(被装饰的)做参数的可调用对象,并且返回一个可调用对象(inner).

注意函数已经有所改变来容纳indent和sort_keys参数。

inner 函数是最终使用 indent 和 sort_keys 参数的. 这没有问题,因为Python的块作用域规则允许这样。使用不同的indent和sort_keys来调用也不成问题,因为inner是本地函数(每次装饰器被使用都会返回一个不同的副本)应用 json_output 函数:

?
1
2
3
@json_output(indent=4)
def do_nothing():
 return {'status': 'done'}

现在运行do_nothing , 会产生一个带缩进的JSON:

?
1
2
>>> do_nothing()
'{\n   "status": "done"\n}'

这是怎么起作用的?

但是,等一等. 如果json_output 不是一个装饰器, 而是一个返回装饰器的函数,为什么它使用起来看着像是一个装饰器?在这里,Python解释器做了什么来让它工作的?更多的解释已经就绪。在这的关键是操作顺序。

特别地,函数调用(json_output(indent=4)) 先于装饰器应用语法(@)被处理 。因此,函数调用的结果会应用给装饰器。

发生的第一件事情是解释器寻找 json_output 函数调用,然后解析这个调用:

?
1
2
3
@json_output(indent=4)
def do_nothing():
 return {'status': 'done'}

json_output 函数所要做的一切就是定义另一个函数, actual_decorator, 然后返回它. 这个函数的结果会提供给@,像下面这样:

?
1
2
3
@actual_decorator
def do_nothing():
 return {'status': 'done'}

现在, actual_decorator 在运行. 它声明另一个本地函数, inner, 并返回它. 像先前讨论过的,这个函数会被指派名字 do_nothing, 被装饰方法的名字. 当do_nothing被调用, inner 函数就会被调用, 运行被装饰方法, JSON使用合适缩进 调用dumps 处理结果

调用签名很重要

当你引进了你的新的,更改过后的json_output函数,你实际引进了一个反向不兼容(backward-incompatible )的改变,意识到这点很重要。

为什么?因为现在期待一个额外的函数调用。如果你想要旧的json_output的行为,不需要任何可用的参数的值,你仍然必须调用这个方法
换句话说,你必须像下面这样做:

?
1
2
3
@json_output()
def do_nothing():
 return {'status': 'done'}

注意圆括号. 它们有影响,因为它们指出了函数正在被调用(即便没有参数),然后结果应用给@.
前面的代码不等价于下面:

?
1
2
3
@json_output
def do_nothing():
 return {'status': 'done'}

这呈现出两个问题。有点让人迷惑,如果你习惯于看到不带签名的装饰器的应用,提供一个空签名的需要就违背直觉。

第二,如果旧的装饰器在你的应用中已经存在,你必须返回并编辑所有它们的现有的调用。如果可能的话,你应该避免反向不减容(backward-incompatible)改变。
完美的情况下,下面三种不同的使用方式,装饰器都会工作

?
1
2
3
@json_output
@json_output()
@json_output(indent=4)

让装饰器基于接收到的参数来改变它的行为是可能的。记住,装饰器只是一个函数,拥有任何其它函数所拥有的所有灵活性,包括对它获取到的输入做出需要做出的响应。

考虑这个对 json_output的更加灵活的迭代:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import functools
import json
class JSONOutputError(Exception):
 def __init__(self, message):
  self._message = message
 def __str__(self):
  return self._message
def json_output(decorated_=None, indent=None, sort_keys=False):
 """Run the decorated function, serialize the result of that function
 to JSON, and return the JSON string.
 """
 # Did we get both a decorated method and keyword arguments?
 # That should not happen.
 if decorated_ and (indent or sort_keys):
  raise RuntimeError('Unexpected arguments.')
 # Define the actual decorator function.
 def actual_decorator(func):
  @functools.wraps(func)
  def inner(*args, **kwargs):
   try:
    result = func(*args, **kwargs)
   except JSONOutputError as ex:
    result = {
    'status': 'error',
    'message': str(ex),
    }
   return json.dumps(result, indent=indent, 
    sort_keys=sort_keys)
  return inner
 #Return either the actual decorator, or the result of applying
 #the actual decorator, depending on what arguments we got.
 if decorated_:
  return actual_decorator(decorated_)
 else:
  return actual_decorator

在目前是不是正作为装饰器使用这一方面,这个函数正努力变得智能。

首先,它确保它不会以出乎意料的方式被调用

你永远不要期待接收被装饰方法同时关键值参数,因为装饰器被调用时总是以被装饰方法作为唯一参数。

第二,它定义了actual_decorator函数,这是要被返回和应用的实际装饰器。它定义了inner 函数,它时从装饰器中返回的最终函数。

最终, 它返回合适结果,这基于它被如何调用:

如果 设置了decorated_ , 它会被作为纯粹的装饰器调用, 没有方法签名,然后它的响应应用给最终装饰器并返回inner函数 . 再次注意使用参数的装饰器如何实际地运作。首先, actual_decorator(decorated_)被调用,解析。然后它的结果(必须是一个可调用对象,因为这是一个装饰器)被调用,inner被提供作为唯一的参数。

如果decorated_没被设置,就会使用关键字参数调用,这个函数必须返回一个实际的装饰器,它接收被装饰方法,并返回inner。因此,这个函数返回actual_decorator
然后这会被python解释器作为实际装饰器(最终返回inner)

为何这个技术有价值?它让你能够先先前使用过的一样管理你的装饰器的功能。意味着你不用去更新已经应用了装饰器的每个地方,但仍然获得了在你需要时添加参数的灵活性。

希望本文所述对大家Python程序设计有所帮助。

原文链接:https://blog.csdn.net/zhangfh1990/article/details/73442447