Python 装饰器(Decorator)

Posted Astropeak

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 装饰器(Decorator)相关的知识,希望对你有一定的参考价值。

装饰器的语法为 @dec_name ,置于函数定义之前。如:

import atexit

@atexit.register
def goodbye():
  print(‘Goodbye!‘)

print(‘Script end here‘)

atexit.register 是一个装饰器,它的作用是将被装饰的函数注册为在程序结束时执行。函数 goodbye 是被装饰的函数。

程序的运行结果是:

Script end here
Goodbye!

可见函数 goodbye 在程序结束后被自动调用。

另一个常见的例子是 @property ,装饰类的成员函数,将其转换为一个描述符。

class Foo:
  @property
  def attr(self):
    print(‘attr called‘)
    return ‘attr value‘

foo = Foo()

等价语法

语句块

@atexit.register
def goodbye():
  print(‘Goodbye!‘)

等价于

def goodbye():
  print(‘Goodbye!‘)
goodbye = atexit.register(goodbye)

这两种写法在作用上完全等价。

从第二种写法,更容易看出装饰器的原理。装饰器实际上是一个函数(或callable),其输入、返回值为:

 说明示例中的对应
输入 被装饰的函数 goodbye
返回值 变换后的函数或任意对象  

返回值会被赋值给原来指向输入函数的变量,如示例中的 goodbye 。此时变量 goodbye 将指向装饰器的返回值,而不是原来的函数定义。返回值一般为一个函数,这个函数是在输入参数函数添加了一些额外操作的版本。

如下面这个装饰器对原始函数添加了一个操作:每次调用这个函数时,打印函数的输入参数及返回值。

def trace(func):
  def wrapper(*args, **kwargs):   1
    print(‘Enter. Args: %s, kwargs: %s % (args, kwargs))   2
    rv = func(*args, **kwargs)   3
    print(‘Exit. Return value: %s % (rv))   4
    return rv

  return wrapper

@trace
def area(height, width):
  print(‘area called‘)
  return height * width

area(2, 3)   5
  1. 1 :定义一个新函数,这个函数将作为装饰器的返回值,来替换原函数
  2. 2, 4 : 打印输入参数、返回值。这是这个装饰器所定义的操作
  3. 3 :调用原函数
  4. 5 :此时 area 实际上是 1 处定义的 wrapper 函数

程序的运行结果为:

Enter. Args: (2, 3), kwargs: {}
area called
Exit. Return value: 6

如果不使用装饰器,则必须将以上打印输入参数及返回值的语句直接写在 area 函数里,如:

def area(height, width):
  print(‘Enter. Args: %s, %s % (height, width))
  print(‘area called‘)
  rv = height * width
  print(‘Exit. Return value: %s % (rv))
  return rv

area(2, 3)

程序的运行结果与使用装饰器时相同。但使用装饰器的好处为:

  1. 打印输入参数及返回值这个操作可被重用

    如对于一个新的函数 foo ,装饰器 trace 可以直接拿来使用,而无须在函数内部重复写两条 print 语句。

    @trace
    def foo(val):
      return ‘return value‘
    

    一个装饰器实际上定义了一种可重复使用的操作

  2. 函数的功能更单纯

    area 函数的功能是计算面积,而调试语句与其功能无关。使用装饰器可以将与函数功能无关的语句提取出来。 因此函数可以写地更小。

    使用装饰器,相当于将两个小函数组合起来,组成功能更强大的函数

修正名称

以上例子中有一个缺陷,函数 areatrace 装饰后,其名称变为 wrapper ,而非 areaprint(area) 的结果为:

<function wrapper at 0x10df45668>

wrapper 这个名称来源于 trace 中定义的 wrapper 函数。

可以通过 functools.wraps 来修正这个问题。

from functools import wraps 

def trace(func):
  @wraps(func) 
  def wrapper(*args, **kwargs):
    print(‘Enter. Args: %s, kwargs: %s % (args, kwargs))
    rv = func(*args, **kwargs)
    print(‘Exit. Return value: %s % (rv))
    return rv

  return wrapper

@trace
def area(height, width):
  print(‘area called‘)
  return height * width

即使用 functools.wraps 来装饰 wrapper 。此时 print(area) 的结果为:

<function area at 0x10e8371b8>

函数的名称能够正确显示。

接收参数

以上例子中 trace 这个装饰器在使用时不接受参数。如果想传入参数,如传入被装饰函数的名称,可以这么做:

from functools import wraps

def trace(name):
  def wrapper(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
      print(‘Enter %s. Args: %s, kwargs: %s % (name, args, kwargs))
      rv = func(*args, **kwargs)
      print(‘Exit %s. Return value: %s % (name, rv))
      return rv

    return wrapped
  return wrapper

@trace(‘area‘)
def area(height, width):
  print(‘area called‘)
  return height * width

area(2, 3)

程序的运行结果为:

Enter area. Args: (2, 3), kwargs: {}
area called
Exit area. Return value: 6

将函数名称传入后,在日志同时打印出函数名,日志更加清晰。

@trace(‘area‘) 是如何工作的?

这里其实包含了两个步骤。 @trace(‘area‘) 等价于:

dec = trace(‘area‘)
@dec
def area(height, width): ...

即先触发函数调用 trace(‘area‘) ,得到一个返回值,这个返回值为 wrapper 函数。 而这个函数才是真正的装饰器,然后使用这个装饰器装饰函数。

多重装饰器

装饰器可以叠加使用,如:

@dec1
@dec2
def foo():pass

等价的代码为:

def foo():pass
foo = dec2(foo)
foo = dec1(foo)

即装饰器依次装饰函数,靠近函数定义的装饰器优先。相当于串联起来。

以上是关于Python 装饰器(Decorator)的主要内容,如果未能解决你的问题,请参考以下文章

python--decorator装饰器

Python – 装饰器(Decorator)

python 装饰器 Decorator

python-闭包和装饰器-02-装饰器(decorator)

python-闭包和装饰器-02-装饰器(decorator)

python中的decorator的作用