专栏首页Astropeak掌握Python 装饰器,其实只需要一盏茶的功夫

掌握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 条评论
登录 后参与评论

相关文章

  • Python:怎样用线程将任务并行化?

    用户2176428
  • 基于隐马尔科夫模型的中文分词方法

    本文主要讲述隐马尔科夫模及其在中文分词中的应用。 基于中文分词语料库,建立中文分词的隐马尔科夫模型,最后用维特比方法进行求解。

    用户2176428
  • ASCII、 Unicode 和 UTF8

    用户2176428
  • Python基础语法(三)——函数

    抽象 抽象 抽象是数学中非常常见的概念。举个例子: 计算数列的和,比如:1 + 2 + 3 + ... + 100,写起来十分不方便,于是数学家发明了求和符号∑...

    zeruns
  • Python装饰器语法与应用#学习猿地

    **在不改变原有函数代码,且保持原函数调用方法不变的情况下,给原函数增加新的功能(或者给类增加属性和方法)**

    学习猿地
  • Python装饰器语法与应用#学习猿地

    **在不改变原有函数代码,且保持原函数调用方法不变的情况下,给原函数增加新的功能(或者给类增加属性和方法)**

    学习猿地
  • Python闭包函数和装饰器

    1.概念:在一个外函数中定义了一个内函数,内函数运用了外函数的临时变量,并且外函数的返回值是内函数的引用 示例代码:演示函数嵌套和闭包。

    潇洒坤
  • Python3中的装饰器

    def foo(): print("第一次定义的foo()函数") def foo(): print("第二次定义的foo()函数") fo...

    py3study
  • Python3匿名函数字典排序、生成式与

    参数类型: 我们经常在看别人的代码中,经常出现def(*args,**kwargs)这样的表现形式;

    py3study
  • Python函数基础

    如果函数体body语句只有一行,或者可以简写为一行,则可以写在def的同行。例如:

    py3study

扫码关注云+社区

领取腾讯云代金券