• Python 进阶:函数装饰器


    一、前言

    本小节主要梳理函数装饰的用法,循序渐进,逐层增加条件,加大复杂度和难度。

    环境说明:Python 3.6、windows11 64位

    二、函数装饰器

    装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。它不修改原来的函数,还给函数增加新的功能,而是使得调用原函数的时候附加一些功能。

    【简单应用】

    装饰器一般是一个闭包函数,只是函数接收的参数是一个函数。如下代码,My_Decorator()就是一个装饰器,直接调用即可。
    以下装饰器是一个比较简单的应用,装饰器 My_Decorator() 给被装饰函数Introduction()加上了一个新的功能【print('print My_Decorator.inner')】,同时又不影响Introduction()原有功能。

    # 定义一个装饰器
    def My_Decorator(func): # 传入被装饰的函数
        def inner():
            print('print My_Decorator.inner') 
            func()          # 调用func() 函数,保证不修改被修饰函数,且正常执行
        return inner        # 返回inner 函数
    
    def Introduction():
        name = 'Xindata'
        print('My name is %s!'%name) # 在装饰器函数中调用的时候才执行
    
    # 调用装饰器函数
    aa = My_Decorator(Introduction)  # 将inner 赋值变量给aa
    print(type(aa)) # 结果为:,此时aa 是一个函数变量
    print(aa)       # 结果为:.inner at 0x000001F6DBBAB790>
    aa()            # 调用aa() 相当于调用inner() 函数,执行结果有2个,一个是【print My_Decorator.inner】是print('print My_Decorator.inner')的结果;一个是【My name is XIndata!】,调用func(),即调用Introduction() 函数的结果。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    【带返回值】

    上文的被装饰函数Introduction()没有返回值,所以可以通过上文代码实现,但是如果有返回值的怎么办呢,方法也很简单,在inner()函数中调用func()前加上return即可获取到被修饰函数的返回值,具体如下:

    # 定义一个装饰器
    def My_Decorator(func): 
        def inner():
            print('print My_Decorator.inner') 
            return func()   # 加上return,将调用func() 函数得到的结果进行返回
        return inner        
    
    def Introduction():
        name = 'Xindata'
        print('My name is %s!'%name) 
        return name         # 将名字返回
    
    aa = My_Decorator(Introduction)  
    print(aa)       # 结果为:.inner at 0x000001F6DAEA33A0>
    my_name = aa()  # 调用aa() 时,同样会打印两个结果:【print My_Decorator.inner】和【My name is XIndata!】
                    # 同时,调用func()(即Introduction())返回的变量name会作为返回值赋值给my_name
    print(my_name)  # 结果为:Xindata
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    【带参数】

    以上代码是被装饰函数Introduction()没有参数的情况下适配的情况,当Introduction()有参数时怎么办呢?下面来看看这个升级版本。
    这个代码的参数值'Xindata'的传递路径比较长,先是通过inner()name参数,然后传递到Introduction()name参数,然后通过Introduction()return 进行返回,再由inner()return 进行返回,最后再赋值给my_name
    image.png

    # 定义一个装饰器
    def My_Decorator(func): 
        def inner(name):
            print('print My_Decorator.inner') 
            return func(name)   # 加上return,将调用func() 函数得到的结果进行返回
        return inner        
    
    def Introduction(name):
    #     name = 'Xindata'
        print('My name is %s!'%name) 
        return name
    
    aa = My_Decorator(Introduction)
    print(aa)       # 结果为:.inner at 0x000001F6DB00FDC0>
    my_name = aa('Xindata') # 调用aa() 即(inner())时,给name参数传递参数值'Xindata'。调用aa() 函数打印的两个结果同上一个代码。
                            # 同时,name的参数值'Xindata'值传递给func()(即Introduction())的参数name,并通过Introduction()的return 返回,然后再由inner() 的return 返回,再赋值给my_name
    print(my_name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    【兼容各种参数】

    以上代码,支持了单一参数的传递,但是作为装饰器,由于其特性,经常会给不同的被修饰函数进行装饰,添加新的功能,而目前的这个装饰器,仅支持单一参数的情况,不能支持多参数,甚至是不同类型的参数,如关键字参数等,为此,需要进一步升级,只需要做一个简单的修改即可,下面看看新的装饰器版本。

    # 定义一个装饰器
    def My_Decorator(func): 
        def inner(*args,**kw):      # 将name参数改为*args,**kw,便可支持各种类型的参数
            print('print My_Decorator.inner') 
            return func(*args,**kw) # 将name参数改为*args,**kw,便可支持各种类型的参数
        return inner        
    
    def Introduction(name):
    #     name = 'Xindata'
        print('My name is %s!'%name) 
        return name
    
    aa = My_Decorator(Introduction)
    print(aa)
    my_name = aa('Xindata')
    print(my_name)
    
    # 如果不需要一些过程,也可以进行链式调用,一步到位
    my_name = My_Decorator(Introduction)('Xindata')
    print(my_name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    @语法糖

    其实上面讲的都是装饰器比较早期的调用方式,后来官方支持了一个更加时髦的写法:@语法糖。把Python之禅的优雅简洁表现得淋漓尽致。下面一起来看看。

    # 定义一个装饰器
    def My_Decorator(func): 
        def inner(*args,**kw):
            print('print My_Decorator.inner') 
            return func(*args,**kw)
        return inner        
    
    # 在Introduction()函数定义前,用@My_Decorator 声明用装饰器My_Decorator()装饰Introduction()函数
    @My_Decorator
    def Introduction(name):
    #     name = 'Xindata'
        print('My name is %s!'%name) 
        return name
    
    my_name = Introduction('Xindata') # 调用方式也发生了一些变化,直接调用被修饰的函数即可
    print(my_name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    【装饰器带参】

    如果需要对不同的场景使用不同的 Introduction(),可以对装饰器再加一层函数,然后在@声明时传递参数,如果是早期的调用方式,则可采用三层链式调用。具体代码如下。

    # 定义一个装饰器
    def Occasion(params):
        def My_Decorator(func): 
            def inner(*args,**kw):
                print('【%s】print My_Decorator.inner'% params) 
                return func(*args,**kw)
            return inner        
        return My_Decorator
    
    @Occasion('Daily')       # 传递参数给params
    def Introduction(name):
        print('My name is %s!'%name) 
        return name
    
    my_name = Introduction('Xindata')
    print(my_name)
    
    # 早期调用方式,如下
    # 定义一个装饰器
    def Occasion(params):
        def My_Decorator(func): 
            def inner(*args,**kw):
                print('【%s】print My_Decorator.inner'% params) 
                return func(*args,**kw)
            return inner        
        return My_Decorator
    
    def Introduction(name):
        print('My name is %s!'%name) 
        return name
    
    my_name = Occasion('Daily')(Introduction)('Xindata') # 三层调用
    print(my_name)
    
    # 两串代码结果都是
    # 【daily】print My_Decorator.inner
    # My name is Xindata!
    # Xindata
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    【多个装饰器】

    单个装饰器,基本讲完了,那多个装饰器呢?
    假设有d1d1两个装饰器,同时装饰f1函数,早期的方法则是val = d1(d2(f));使用@则是把@d1@d2两个装饰器按顺序应用到f函数上。抽象代码如下:

    @d1
    @d2
    def f():
    	print('f')
    val = f()
        
    # 等同于
    def f():
    	print('f')
    val = d1(d2(f))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那上面的例子,再加一个装饰看看效果:

    # 定义一个装饰器
    def Occasion(params):
        def My_Decorator(func): 
            def inner(*args,**kw):
                print('【%s】print My_Decorator.inner'% params) 
                return func(*args,**kw)
            return inner        
        return My_Decorator
    
    def Current_Decorator(func): 
        def inner(*args,**kw):
            print('print Current_Decorator.inner') 
            return func(*args,**kw)
        return inner        
    
    @Occasion('Daily')         
    @Current_Decorator         # 新增装饰器
    def Introduction(name):
        print('My name is %s!'%name) 
        return name
    
    my_name = Introduction('Xindata')
    print(my_name)
    
    
    ## 早期调用方式
    # 定义一个装饰器
    def Occasion(params):
        def My_Decorator(func): 
            def inner(*args,**kw):
                print('【%s】print My_Decorator.inner'% params) 
                return func(*args,**kw)
            return inner        
        return My_Decorator
    
    def Current_Decorator(func): 
        def inner(*args,**kw):
            print('print Current_Decorator.inner') 
            return func(*args,**kw)
        return inner   
    
    def Introduction(name):
        print('My name is %s!'%name) 
        return name
    
    my_name = Occasion('Daily')(Current_Decorator(Introduction))('Xindata') # 加一层调用
    print(my_name)
    
    # 两串代码结果都是
    # 【Daily】print My_Decorator.inner
    # print Current_Decorator.inner
    # My name is Xindata!
    # Xindata
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    看完上面这两个代码,你可能犯迷糊了,看打印结果前两行是先执行Occasion('Daily')装饰器,再执行Current_Decorator装饰器。但是看早期调用方式,又不太像,因为Occasion('Daily')执行完这一层,就跑到 Current_Decorator(Introduction)这一层,最后再传递 'Xindata'调用Introduction()。没错!事实就是第一个装饰器执行一半就跑到第二个装饰器执行,如果有第三个装饰器,还会跑到第三个装饰器,执行完第三个装饰器之后,再执行第二个装饰器,再执行第一个装饰器,从外到里再到外,这有点像递归的思想。
    为了更好看这个效果,下面抽象一个代码在看看这个效果:

    def Activity(func):
        def wrapper1(*args,**kw):
            print('活动开始了!  内层函数第1个执行,func参数值:%s'% func.__name__)
            func(*args,**kw)
            print('活动结束了!')
        print('外层函数第3个执行:Activity,返回wrapper1,开始执行内层函数,从wrapper1开始')
        return wrapper1
    
    def Timing(func): 
        def wrapper2(*args,**kw):
            print('介绍计时开始!内层函数第2个执行,func参数值:%s'% func.__name__)
            func(*args,**kw)
            print('介绍计时结束!')
        print('外层函数第2个执行:Timing,返回wrapper2 给 @Activity')
        return wrapper2     
    
    def Speech(func):
        def wrapper3(*args,**kw):
            print('开始演讲。    内层函数第3个执行,func参数值:%s'% func.__name__)
            func(*args,**kw)
            print('结束演讲。')
        print('外层函数第1个执行:Speech,返回wrapper3 给 @Timing')
        return wrapper3
    
    @Activity
    @Timing
    @Speech
    def Introduction(name):
        print('My name is %s!'%name) 
    
    Introduction('Xindata')
    
    # 运行结果为:
    # 外层函数第1个执行:Speech,返回wrapper3 给 @Timing
    # 外层函数第2个执行:Timing,返回wrapper2 给 @Activity
    # 外层函数第3个执行:Activity,返回wrapper1,开始执行内层函数,从wrapper1开始
    # 活动开始了!  内层函数第1个执行,func参数值:wrapper2
    # 介绍计时开始!内层函数第2个执行,func参数值:wrapper3
    # 开始演讲。    内层函数第3个执行,func参数值:Introduction
    # My name is Xindata!
    # 结束演讲。
    # 介绍计时结束!
    # 活动结束了!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    执行顺序如下:
    image.png
    外层函数起装饰效果,内层函数执行新功能。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。也就是说,外层函数即使不调用函数**Introduction()**也会打印出来。(可以注释掉Introduction('Xindata')运行看看效果。)
    所以越靠近被装饰函数的装饰器先装饰后执行,而离得远的相反。可以记为就近原则装饰,执行则相反(类似递归思想)

    二、变量作用域

    【加上变量】

    装饰函数一般都是一个闭包函数,这里单独拿闭包函数来做说明,重新找一个比较简单的例子辅助理解。
    当在闭包函数的外层函数定义一个变量时,内层函数可以直接调用。

    def func():
        x = 0
        def inner():
            # 调用x
            return x
        return inner
    
    f = func()
    print(f()) # 结果为0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果在内层函数要对外层的变量进行修改,这时便会报错,这个和普通函数调用全局变量又要修改它的场景差不多,只不过在那个场景下,可以通过在函数内部对变量使用global声明为全局变量解决该问题。
    那闭包函数是不是也有相关的关键字来声明将x作为闭包函数的“全局变量”呢?还真有,它就是nonlocal
    image.png
    使用 nonlocal x声明x不是当前内层函数的局部变量。
    注意:这里的nonlocal x并不是声明x为全局变量。

    def func():
        x = 0
        def inner():
            nonlocal x
            x += 1
            return x
        return inner
    
    f = func()
    print(f()) # 结果为1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    【多次调用】

    当多次调用f()的时候,我们会发现结果是递增的,但是打印x时则报错变量未定义。那么x只是闭包函数内的局部变量,并不是全局变量,但是它是怎么把值进行传递的呢?因为这个x是一个特殊的变量,有一个技术术语,叫自由变量(free variable),指未在本地作用域中绑定的变量。闭包函数会保留自由变量的绑定,使得调用函数时可以继续使用只有变量。

    def func():
        x = 0
        def inner():
            nonlocal x
            x += 1
            return x
        return inner
    
    f = func()
    print(f()) # 结果为1
    print(f()) # 结果为2
    print(f()) # 结果为3
    print(x)   # 报错:NameError: name 'x' is not defined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    可以通过一些属性查看相关的变量和值
    f.__closure__中的各个元素对应于f.__code__.co_freevars中的一个名称。这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。

    f.__code__.co_varnames  # 查看当前函数的局部变量
    f.__code__.co_freevars  # 查看当前函数的自由变量
    f.__closure__[0].cell_contents # 查看自由变量的值
    
    • 1
    • 2
    • 3


    - End -

  • 相关阅读:
    2023天津公租房网上登记流程图,注册到信息填写
    PT_离散型随机变量下的分布:几何/超几何/幂律
    一对一语音直播系统源码——如何解决音视频直播技术难点
    在excel内处理二进制和十六进制数据
    【Mysql】Mysql的数据类型
    商场百货数字化会员系统引流方式 购物中心线上会员拉新
    IDEA利用maven建立javaWeb项目(IDEA 2021.3.3)
    使用Go语言和chromedp库下载Instagram图片:简易指南
    2023.10.14 培训总结
    form表单,formdata对象,实现文件上传
  • 原文地址:https://blog.csdn.net/qq_45476428/article/details/126962919