掌握Python 装饰器,其实只需要一盏茶的功夫

装饰器的语法为 @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 装饰器80%的知识,并能够应用到工作学习中。你知道了装饰器的工作原理,以及如何自己编写一个装饰器,及避免常见的编写错误。

如果你仍然有疑问,欢迎留言讨论!(如果你觉得这篇文章有用,请点下方推荐按钮:p)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

ASP.NET MVC以ModelValidator为核心的Model验证体系: ModelValidatorProviders

前面篇文章我们分别介绍用真正用于实施Model验证的ModelValidator(《ASP.NET MVC以ModelValidator为核心的Model验证体...

1645
来自专栏Vamei实验室

Python深入05 装饰器

装饰器(decorator)是一种高级Python语法。装饰器可以对一个函数、方法或者类进行加工。在Python中,我们有多种方法对函数和类进行加工,比如在Py...

19210
来自专栏全沾开发(huā)

数组的遍历你都会用了,那Promise版本的呢

1954
来自专栏张善友的专栏

Edge.js:让.NET和Node.js代码比翼齐飞

通过Edge.js项目,你可以在一个进程中同时运行Node.js和.NET代码。在本文中,我将会论述这个项目背后的动机,并描述Edge.js提供的基本机制。随后...

1936
来自专栏Java帮帮-微信公众号-技术文章全总结

Java设计模式-组合模式

组合模式: 将对象组合成树形结构以表示‘部分-整体’的层次结构, 使得用户对单个对象和组合对象的使用具有一致性. 解析 组合模式描述了如何将容器和叶子节点进行...

3416
来自专栏CodeSheep的技术分享

Java编程思想学习录(连载之:内部类)

19811
来自专栏landv

用C语言实现窗口抖动

1406
来自专栏SHERlocked93的前端小站

JS 利用高阶函数实现函数缓存(备忘模式)

高阶函数就是那种输入参数里面有一个或者多个函数,输出也是函数的函数,这个在js里面主要是利用闭包实现的,最简单的就是经常看到的在一个函数内部输出另一个函数,比如

813
来自专栏程序小工

【实战】Tp5+小程序(一)--数据库访问与ORM

ThinkPHP5 从入门到深入学习,结合实战项目深入理解 ThinkPHP5 的特性和使用方法,了解 ThinkPHP5 的数据库访问和 ORM 思想,学习使...

1372
来自专栏逸鹏说道

现在无法开始异步操作。异步操作只能在异步处理程序或模块中开始,或在页生存期中的特定事件过程中开始

异常处理汇总-后端系列 http://www.cnblogs.com/dunitian/p/4523006.html 这篇没啥技术含量,用来小记一番 错误信息 ...

3815

扫码关注云+社区