当前位置:   article > 正文

一篇文章汇总Python装饰器全知识图谱(使用场景,基本用法,参数传递,闭包操作,类装饰器和AOP)_python 装饰器与装饰对象的数据共享

python 装饰器与装饰对象的数据共享

装饰器,是将Python代码变得低耦合,简洁优美的必经之路,同时也是实现闭包操作,AOP编程的基础。这一篇博客从装饰器的产生原因,基本使用,延伸到参数传递,闭包操作,最后到类装饰器和AOP,希望能用我自己掌握的知识,尽量详细的对装饰器的知识点进行全面总结和梳理。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

操作环境

以下所有操作都在下面的环境中完成:

  • Python 3.7.1
  • IPython 7.2.0

Python中的函数

首先要理解一个概念,Python中的函数名也是一个变量。这个变量的值可以赋值给别的变量,可以传递给函数,也可以被函数返回。

来看示例。

函数名就是变量

In [1]: def test(): print('test')                                                                                                                                                     

In [2]: print(test)                                                                                                                                                          
<function test at 0x7fc099215a60>
  • 1
  • 2
  • 3
  • 4

如上所示,打印函数名返回的是函数在内存中的地址。

补充,普通变量获取内存地址可以用print("%x" % id(test)),其中id返回十进制的地址,通过%x转为十六进制

函数名可以赋值给别的变量

In [10]: xiaofu = test                                                                                                                                                       

In [11]: print(xiaofu)                                                                                                                                                       
<function test at 0x7fc099215a60>

In [12]: xiaofu()                                                                                                                                                             
test
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

函数名可以传递给函数参数

In [5]: def func1(): print('666')                                                                                                                                            

In [6]: def func2(func): func()                                                                                                                                              

In [7]: func2(func1)                                                                                                                                                         
666
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数也可以返回函数名

In [8]: def wrapper():
   ...:     def inner():
   ...:         print('inner')
   ...:     return inner
   ...:                                                                                                                                                                      

In [9]: wrapper()()                                                                                                                                                          
inner
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里的第一个小括号是执行wrapper,第二个小括号是执行返回的inner。而返回的inner函数并不一定要马上执行

In [11]: xiaofu=wrapper()                                                                                                                                                    

In [12]: xiaofu()                                                                                                                                                            
inner
  • 1
  • 2
  • 3
  • 4

之所以要先说一说函数,是因为装饰器是对函数的一个升级。

什么是装饰器

好的程序应该有较低的耦合性,同时减少重复代码。

想象这么一个问题,你已经定义好了很多个函数,每个函数有自己的功能。现在有一个新的需求,想对所有函数加一个功能,执行的时候记录起始时间和结束时间。该如何实现呢?

比较容易想到的就是在每个函数中添加两行打印时间的代码,但是这样造成了所有函数中都有一段重复的代码,并且打印时间这个功能和函数的具体功能无关,出于低耦合的考虑也不应该混合在一起。

比较理想的情况就是原有的所有函数都原封不动,单独创建一个函数实现打印时间功能。利用上面说的函数特性,构建一个新函数如下


In [13]: def record_time(func):
    ...:     print(time.time())
    ...:     func()
    ...:     print(time.time())
    ...:                                                                                                                                                                     

In [14]: import time                                                                                                                                                         

In [15]: def test():
    ...:     print('666')
    ...:                                                                                                                                                                     

In [16]: record_time(test)                                                                                                                                                   
1586531635.098499
666
1586531635.0986056
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这样子实现逻辑上没问题,但是问题就是我们在调用函数的时候并不是原先的业务函数test,而变成了record_time,影响了原本代码的结构。那么有没有什么更好的办法呢?

有。通过在record_time函数内返回一个增强了原函数功能的新函数就可以达到目的。

def test():
    print('666')


def record_time(func):
    def inner(*args, **kwargs):
        print(time.time())
        func()
        print(time.time())

    return inner
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这时候如果将返回的inner函数再重新赋值给test就完成了对test的增强目的

test = record_time(test)
test()
  • 1
  • 2

这里的record_time函数其实就是一个简单的装饰器(decorator)了,它也是一个函数,有唯一一个参数是一个函数名,同时在装饰器里又定义了一个新函数,并且把传递进来的函数包裹在其中,添加了一些新的功能,就像进行了一番装饰。最后将这个经过了装饰的新函数再次返回,达到目的

@语法糖

功能虽然实现了,但是我们也并不想每次调用test的时候都对其来一次重新赋值。于是python引入了@这个符号做为装饰器的语法糖。将被装饰的函数上面用@指定装饰器的函数名即可

def record_time(func):
    def inner(*args, **kwargs):
        print(time.time())
        func()
        print(time.time())

    return inner


@record_time
def test():
    print('this is crazy')


if __name__ == '__main__':
    # test = record_time(test)
    test()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到,除了增加@语法,原先的函数test在定义和调用的时候都不需要任何的修改。不过这里需要注意的是,被装饰的函数的定义一定要在装饰器函数的后面,不然会报错

装饰器的多种参数

装饰器的一个基本结构,就是一个函数嵌套着另外一个函数。上面的例子中内外两个函数都没有任何参数,如果遇到带参数的函数又该如何实现装饰器呢?

被装饰函数的参数

如果是被装饰的函数带参数,只需要对装饰器内的inner函数指定对应参数即可

def record_time(func):
    def inner(m, n):
        print(time.time())
        func(m, n)
        print(time.time())

    return inner


@record_time
def test(a, b):
    print(a + b)


if __name__ == '__main__':
    test(1, 3)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

返回

1586579728.1995654
4
1586579728.1995995
  • 1
  • 2
  • 3

但是这个装饰器又不是给你这一个函数用,万一别的函数不是2个参数怎么办呢?

也很好办,直接利用*args**kwargs来代替参数即可

def record_time(func):
    def inner(*args, **kwargs):
        print(time.time())
        func(*args, **kwargs)
        print(time.time())

    return inner


@record_time
def test(a, b):
    print(a + b)


if __name__ == '__main__':
    test(1, 3)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

运行结果没有任何问题。

这样一来,不管被装饰函数的参数是位置参数或者关键字参数都可以顺利传递进去了。

装饰器函数的参数

为了实现更为灵活和强大的功能,装饰器函数也可以传递参数,例如@record_time(level),这样通过不同参数实现不同功能。这里的修改看仔细了

def record_time(level):
    def wrapper(func):
        def inner(*args, **kwargs):
            if level == 1 or level == 3:
                print(time.time())
            func(*args, **kwargs)
            if level == 2 or level == 3:
                print(time.time())
        return inner
    return wrapper


@record_time(1)
def test(a, b):
    print(a + b)


if __name__ == '__main__':
    test(1, 3)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这个装饰器会查看传递进来的level参数,如果是1,只打印开始时间;如果是2,只打印结束时间;如果是3,两个时间都打印。这里的装饰器函数record_time相当于在原先的装饰器上又封装了一层,返回一个装饰器。

如果觉得封装来封装去不好理解也没关系,只需要知道装饰器函数如果带参数,就要按照上面的方式嵌套2个函数即可,最外面的装饰器函数接受@装饰器时候传递的参数,第一层嵌套的函数只接受函数名参数,第二层嵌套的函数接受被装饰函数的参数。

装饰器中的变量

前面只是装饰器的基本使用,下面要说的会比较进阶一点。

什么是闭包和闭包函数

首先来两个定义。

什么是闭包?如果一个函数里面定义了另一个函数,并且内部函数引用了外部函数作用域而非全局作用域的变量的情况,叫做闭包(closure)。

什么是闭包函数?在闭包情况下的那个内部函数就叫做闭包函数(closure function)。

来看示例。

def wrapper_():
    a = 1
    def inner():
        print(a)
    inner()
    print(inner.__closure__)

if __name__ == '__main__':
    wrapper_()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里内部函数inner引用了外部函数定义的一个变量a,所以形成了闭包,而inner就是一个闭包函数。

要如何判断一个函数是不是闭包函数呢?可以通过打印func.__closuer__,如果结果是cell表示是闭包函数,如果结果是None则不是闭包。

运行上面程序的结果如下

1
(<cell at 0x7f27e00df678: int object at 0x564cb053c380>,)
  • 1
  • 2

很明显函数inner是一个闭包函数。

而如果把程序改一下

a = 1
def wrapper_():
    # a = 1
    def inner():
        print(a)
    inner()
    print(inner.__closure__)

if __name__ == '__main__':
    wrapper_()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

返回的结果为

1
None
  • 1
  • 2

表明inner这时候不是闭包函数,因为它引用的a是一个全局变量,而不是外部函数作用域的变量。

现在我知道了闭包就是函数的嵌套,并且内层函数引用了外层函数定义的变量,这有什么用呢?

变量保持

闭包的好处就是可以实现变量保持。

在闭包场景,外层函数如果像装饰器那样将闭包函数进行了返回,那么外界就获得了闭包函数的内存地址,以后随时可以运行闭包函数。但是问题是闭包函数引用了外层函数的变量,我们知道一个函数运行完毕,其内部定义的局部变量会释放,那闭包函数在运行的时候就会出错。

所以Python中规定,如果是闭包函数,其引用的外层函数的变量不会随着外层函数的运行结束而消亡,会一直保持在内存中

在内存中一直有个变量可以操作,这是不是听起来和Redis缓存很像,没错,利用变量保持,我们可以实现很多类似Redis缓存的功能

例如每次运行闭包函数都往里加入新名单的一个名单列表,或者从磁盘读取的每次运行闭包函数都会重复利用的数据。

下面来两个例子感受一下。

闭包变量保持示例一

我想设计一个函数,每次运行会读取txt文件中的某一行。用两种方式来写这个函数

def read_file():
    with open('/home/fuhx/Desktop/dmbj04.txt', 'r') as f:
        all_lines = f.readlines()
        # return len(all_lines)
    def read_one_line(n):
        return all_lines[n]
    return read_one_line

def read_line(n):
    with open('/home/fuhx/Desktop/dmbj04.txt', 'r') as f:
        all_lines = f.readlines()
        return all_lines[n]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这是一份3000多行的文本文件,函数read_line每次执行会去读取文件,并从中获取一行。而函数read_file是一个闭包结构,因为外层函数的变量all_lines被闭包函数read_one_line引用,所以会一直在内存保持,以后每次调用闭包函数都是直接从内存中获取文件内容。

下面比较下连续获取3行内容的速率

if __name__ == '__main__':
    lines = read_file()
    print(time.time())  #  起始时间
    print(lines(10))
    print(lines(11))
    print(lines(12))
    print(time.time())  # 闭包三次之后时间
    print(read_line(10))
    print(read_line(11))
    print(read_line(12))
    print(time.time())  # 常规三次之后时间
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

最后结果如下

1586600444.1975882
  我胸口和左手打着石膏,介是不知道自己伤的多重,听他一说,才知道自己命大。我又问他大概什么时候能出院,他对我笑笑,说没十天半个月,连床都下不了。  

  当天晚上,送我过来的武警听说我能说话了,带了水果篮过来看我,我又问了他问医生同样的话,他也不知道如何回答我,只说有几个村民在蓝田的一条溪边找到了我,我是给放在一个竹筏上,身上的伤口已经简单处理过了,医生说道,要不是这些处理,我早就死了。  

  我觉得奇怪,我最后的记忆是落进水里的那一刹那,按道理最多也是应该给水冲到河滩上,怎么给放到竹筏上去了,二来,蓝田那里离夹子沟那一带有七八里路呢,难道,我们在地下河走过的路,不知不觉已经有这么长一段距离了?  

1586600444.1977124
  我胸口和左手打着石膏,介是不知道自己伤的多重,听他一说,才知道自己命大。我又问他大概什么时候能出院,他对我笑笑,说没十天半个月,连床都下不了。  

  当天晚上,送我过来的武警听说我能说话了,带了水果篮过来看我,我又问了他问医生同样的话,他也不知道如何回答我,只说有几个村民在蓝田的一条溪边找到了我,我是给放在一个竹筏上,身上的伤口已经简单处理过了,医生说道,要不是这些处理,我早就死了。  

  我觉得奇怪,我最后的记忆是落进水里的那一刹那,按道理最多也是应该给水冲到河滩上,怎么给放到竹筏上去了,二来,蓝田那里离夹子沟那一带有七八里路呢,难道,我们在地下河走过的路,不知不觉已经有这么长一段距离了?  

1586600444.2049508
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以看出,闭包三次耗时约0.2ms,而常规三次耗时约7ms,如果是多次的操作差距简直不是一个量级的。所以多次重复I/O操作的数据可以用闭包来加快速度

闭包变量保持示例二

既然是讲装饰器,铺垫了这么久,最终的目的也是为了将闭包和装饰器结合起来。

例如想对某一个函数加限制,最多每5秒执行一次,否则提示不允许执行。

这时候就可以将上一次的执行时间用变量保持的方式来储存,以后每次执行前进行查询以及更新。注意因为涉及到更新操作,所以用list或者dict类型比较好,因为是可变类型,不然每次更新都要消耗内存,如下

interval = 5
def limit_time(func):
    last_time = [0]
    def inner(*args,**kwargs):
        gap = time.time()-last_time[0]
        if gap > interval:
            func()
            last_time[0]=time.time()
        else:
            print('Please wait for another {} seconds'.format(str(int(interval-gap))))
    return inner


@limit_time
def test2():
    print('xiaofu')


if __name__ == '__main__':
    test2()
    time.sleep(3)
    test2()
    time.sleep(7)
    test2()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

执行的结果如下

xiaofu
Please wait for another 2 seconds
xiaofu
  • 1
  • 2
  • 3

当然也可以用带参数的装饰器来实现,把interval做为装饰器的参数来传递进去,大家可以自己练习一下。

类装饰器

前面的装饰器都是用函数来实现的,而如果想实现更为灵活和丰富的功能,还可以用类来做装饰器。类的__init__方法就相当于外层函数,而__call__方法就相当于内层函数。

例如将上面的限制访问的装饰器修改一下

class limit_time:
    def __init__(self, func):
        self.interval = 5
        self.__func = func
        self.last_time = [0]

    def __call__(self, *args, **kwargs):
        self.gap = time.time() - self.last_time[0]
        if self.gap > self.interval:
            self.__func()
            self.last_time[0]=time.time()
        else:
            print('Please wait for another {} seconds'.format(str(int(self.interval-self.gap))))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

因为类经过实例化后的数据会一直在,所以就没有上面说的闭包变量保持的必要。

让装饰器更逼真

装饰器使得函数可以在几乎不做改变的情况下添加额外功能,但是其实原函数已经被装饰器的内层函数重新赋值,所以一些函数的元信息都变了。还是用前面限制函数调用频率的装饰器

interval = 5
def limit_time(func):
    last_time = [0]
    def inner(*args,**kwargs):
        gap = time.time()-last_time[0]
        if gap > interval:
            func()
            last_time[0]=time.time()
        else:
            print('Please wait for another {} seconds'.format(str(int(interval-gap))))
    return inner

@limit_time
def test2():
    print('xiaofu')

if __name__ == '__main__':
    test2()
    print(test2.__name__)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

结果如下

xiaofu
inner
  • 1
  • 2

可以看到test2.__name__返回的确实装饰器的内层函数inner的信息。有没有办法让原函数只是功能增加,其余所有信息都原封不动呢?

Python已经帮我们考虑好了这个问题。通过在装饰器的内层函数加一个额外装饰器来达到目的

interval = 5
def limit_time(func):
    last_time = [0]
    @wraps(func)
    def inner(*args,**kwargs):
        gap = time.time()-last_time[0]
        if gap > interval:
            func()
            last_time[0]=time.time()
        else:
            print('Please wait for another {} seconds'.format(str(int(interval-gap))))
    return inner
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

唯一的区别就是这个wraps(func)装饰器,会将参数中的函数元信息复制到inner,所以返回之后就跟原函数一样了。使用前记得先from functools import wraps

多重装饰器

一个函数有可能不只一个装饰器,例如

@a
@b
@c
def func():
    pass
  • 1
  • 2
  • 3
  • 4
  • 5

这个时候是按照从下往上的顺序进行装饰,也就是类似于

func = a(b(c(func)))
  • 1

装饰器与AOP

AOP,全称为Aspect Oriented Programming,面向切面编程,是面向对象编程(OOP)的补充。面向切面编程,指的是类和方法加载时,动态地将代码切入到类的指定方法、指定位置上的编程思想,用于在不改变原方法的前提下添加额外功能。

切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。用装饰器的角度来看,装饰器的功能就是切面,被装饰的方法就是切入点

Python的装饰器使得Python对AOP的实现有得天独厚的优势,例如Django中的中间件Middleware。

总结

从产生原因,基本使用方法,到传入参数和闭包变量保持,最后到类装饰器和AOP,希望这一篇文章能将Python装饰器的知识点尽量全的整理下来。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/336302
推荐阅读
相关标签
  

闽ICP备14008679号