Python装饰器实例讲解(三)

时间:2023-02-16 14:06:09

本文多参考《流畅的python》,在此基础上增加了一些实例便于理解

姊妹篇

Python装饰器实例讲解(一),让你简单的会用

Python装饰器实例讲解(二),主要讲了一个万能公式(原理)

本文其实反而是最最基础的部分,当然也回答了好几个关键的问题,也有一些是重复的地方

  • 理解装饰器必须理解函数、闭包等概念
  • 闭包后面单独讲,函数在本文是重点,从函数讲起

函数:一等对象

  • 在Python中,函数是一等对象,需要满足以下条件:
    • 在运行时创建
    • 能赋值给变量或数据结构中的元素
    • 能作为参数传给函数
    • 能作为函数的返回结果
  • 在Python中,整数、字符串和字典都是一等对象

函数名能赋值给变量

  • 示例

    def func():    
        print('hello')
    
    my_func = func   # 此处不要写成func()  
    my_func()  # hello
    func() # hello
    
  • 这样的使用比比皆是,比如在pytest中的一个应用

    import pytest
    
    xfail = pytest.mark.xfail  # 就是这里
    
    
    @xfail  # 这样看就比较简洁了
    def test_hello():
        assert 1
    
    if __name__ == '__main__':
        pytest.main(['-sv',__file__])
    
  • 较为为典型的应用就是lambda,它是匿名的,但它同样可以赋值给一个变量

    my_add = lambda x,y:x+y
    result = my_add(1,2)
    print(result)  # 3
    

函数能作为参数传给函数

  • 示例

    def double(x):
        return x*2
    
    def triple(x):
        return x*3
    
    def calc(funcion_name,x):
        return funcion_name(x)
    
    print(double(2)) # 4
    print(triple(2)) # 6 
    print(calc(double,2)) # 4
    print(calc(triple,2)) # 6
    
  • 在上面的例子中你可以看到calc这个函数接收的第一个参数是函数名字

  • 调用的时候你传入的是double、triple这样的名字

  • 仔细观察代码,calc的实现其实的本意就是把第一个参数当做函数名,第二个参数是第一个参数的参数。所以本质上你可以做任何事情,只要这个函数仅接收一个参数即可

    print(calc(bin,10))  # 返回的是bin(10)的结果  0b1010  
    print(calc(max,(2,5,3)))  # 执行的是max((2,5,3))  
    
  • 高阶函数如map/filter/reduce/sort等,如果你接触过,他们的参数不都是函数名吗?

  • 我也写过一篇文章,Python函数式编程之map/filter/reduce/sorted

能作为函数的返回结果

  • 示例

    def add(x,y):
        return x+y
    
    def func():
        print('calling func')
        return add
        
    print(func()(1,2)) 
    # 输出如下
    # calling func
    #  3
    # func() 就是 add , 跟你执行add(1,2)的效果是一样的
    
  • 你也可以这样

    new_add = func()
    print(new_add(1,2)) 
    # calling func
    #  3
    
  • 如果你看过前面的两篇文章,到这里就应该很熟悉了

可调用对象

  • 除了函数是可调用的,还有很多(其实也没多少)都是可调用对象

  • 按照流畅的python的说法,有这么多可调用对象

    可调用对象 说明
    用户定义的函数 使用 def 语句或 lambda 表达式创建
    内置函数 使用 C 语言(CPython)实现的函数,如 len 或 time.strftime
    内置方法 使用 C 语言实现的方法,如 dict.get
    方法 在类的定义体中定义的函数
    调用类时会运行类的 new 方法创建一个实例,然后运行 init 方法,初始化实 例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。
    类的实例 如果类定义了 call 方法,那么它的实例可以作为函数调用。
    生成器函数 使用 yield 关键字的函数或方法。调用生成器函数返回的是生成器对象。

对普通的初学者而言其实就是函数和类,类的调用分2级,Obj()这是实例化,同时调用new和init。

new和init魔术方法,后面会单独开篇讲解,单例跟这个是息息相关的。

生成器后面也考虑单独开文章说一下。

  • 示例代码(说明new和init)

    class Person:
        def __new__(cls, *args, **kwargs):
            print('calling new')
            cls.instance = super().__new__(cls)
            return cls.instance
        def __init__(self):
            print('calling init')
    
    wuxianfeng  = Person()
    
  • 示例输出

    calling new
    calling init
    
  • 但此时wuxianfeng这个Person类的实例并不是可调用的对象

  • 如果你写wuxianfeng(),会给你提示

    TypeError: 'Person' object is not callable
    
  • 你需要在Person类中定义一个__call__方法

    class Person:
        ...
        def __call__(self, *args, **kwargs):
            print('callable')
    
  • 此时再次执行wuxianfeng()就可以得到callable了

  • 当然如果你执行Person()()结果也是这样的

    calling new
    calling init
    callable
    

  • python提供了一个内置的callable()函数来检测对象是否可调用

    print([callable(obj) for obj in (abs, str, 13)])  # [True, True, False]
    

回到装饰器

  • 虽然你可能已经学到装饰器三了,但请你清空下你了解的装饰器,倒也不是从0开始,带点复习

  • 示例代码

    def decorate(function_name):
        def inner():
            print('calling inner')
            function_name()
        return inner
    @decorate
    def target():
        print('calling target')
    
    target()
    
  • 输出结果

    calling inner
    calling target
    

  • 根据万能公式,分析下执行过程

    • 当你在执行target()的时候,由于target上有个装饰器,实际上发生的事情是target = decorate(target)

    • 前面的target 是新的(一个变量),后面的decorate(target)中的target是你之前定义的函数

    • decorate(target)就会去调用decorate函数传入target参数,返回inner

    • 卡....返回了inner,是你加了装饰器的效果,至此都没有执行函数

    • 正是由于最终的target(),就是去调用了inner(),对应的语句是

      print('calling inner')
      function_name()  # 你传入的是target就是此处的function_name
      

  • 说一些理论
    • 装饰器只是语法糖
    • 装饰器可以像常规的可调用对象那样调用,其参数是另一个函数(被装饰的函数)。
    • 装饰器可能会处理被装 饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
    • 装饰器的一大特性是,能把被装饰的函数替换成其他函数
    • 第二个特性是,装饰器在加载模块时立即执行

  • 关于被替换

    def decorate(function_name):
        def inner():
            print('calling inner')
            function_name()
        print('这是inner的id:',id(inner))
        return inner
    @decorate
    def target():
        print('calling target')
    
    
    print('这是target的id:',id(target))
    
    
  • 示例输出(你输出的id跟我肯定不一样,但2者应该是一致的,从这个角度也能看出来你执行的target不再是原来的target了)

    这是inner的id: 1804087435904
    这是target的id: 1804087435904
    

叠放装饰器

  • 日常代码中还是有一些场景能看到一个函数被多个装饰器装饰的情况,比如pytest的allure

  • 这个执行顺序就是如你所想的那般,先装饰的先执行

  • 示例代码

    def decorate1(function_name):
        def inner1():
            print('calling inner1')
            function_name()
        return inner1
    
    def decorate2(function_name):
        def inner2():
            print('calling inner2')
            function_name()
        return inner2
    
    @decorate1
    @decorate2
    def target():
        print('calling target')
    
    target()  
    # 输出
    # calling inner1
    # calling inner2
    # calling target
    
  • 但这种情况下的万能公式是怎样的呢???你知道不~

  • 万能公式1

    @decorate1
    def target():
        print('calling target')
        
    # 等价于做了一件事
    target = decorate1(target)
    
  • 万能公式2

    @decorate1
    @decorate2
    def target():
        print('calling target')
     
    # 等价于做了2件事
    # 第一件事,注意,就近原则
    target = decorate2(target)  # 前面的target是新的变量,后面的target是def的最初的、原始的函数
    # 第二件事
    target = decorate1(target)  # 前面的target又是一个新的变量,后面的target是line8的前面的target
    
    # 你也可以理解为做了一件事(合并上面2行)
    target = decorate1(decorate2(target) )  # 最近的@的先调用
    
  • 不信请看

    def decorate1(function_name):
        def inner1():
            print('calling inner1')
            function_name()
        return inner1
    
    def decorate2(function_name):
        def inner2():
            print('calling inner2')
            function_name()
        return inner2
    
    def target():
        print('calling target')
    
    target = decorate2(decorate1(target) )
    target()
    

  • 装饰器就讲到这里了
  • 会用是第一步,理解简单的过程是第二步,会写一个装饰器才算是基本懂了