zoukankan      html  css  js  c++  java
  • 掌握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)

  • 相关阅读:
    我在项目内使用了设计模式后,同事直呼看不懂
    pom文件中依赖找不到的根本解决方法
    基于session的传统认证授权详解
    python中2个字典比较
    编码设计应遵循的规则
    yarn任务执行流程
    python3 中print 显示不全问题
    pandas 可视化
    python时间大小判断,相差天数秒数计算
    Impala任务程序cancle
  • 原文地址:https://www.cnblogs.com/astropeak/p/9029104.html
Copyright © 2011-2022 走看看