python进阶之装饰器

2020-01-20 11:37:34 浏览数 (1)

一.无参装饰器

问题:如何计算一段程序的运行时间?

先看一段简单代码:

代码语言:javascript复制
1 import time
2 def func():
3     start = time.time() # 记录程序开始时间
4     time.sleep(5)
5     print('程序正在运行......')
6     endtime = time.time() # 记录程序运行结束时间
7     print(endtime-start) # 打印程序运行时间
8 # 调用函数   
9 func()

输出:

代码语言:javascript复制
程序正在运行......
5.00543737411499

上面的代码是不是就实现了计算程序运行时间的目的,那么如果我想计算别的函数的运行时间是不是也要在函数内部加上start和endtime来计算时间的语句,是不是超级麻烦

下面我们修改一下上面的代码,实现计算任何函数的运行时间:

代码语言:javascript复制
 1 import time
 2 def func():
 3     time.sleep(5)
 4     print('程序正在运行......')
 5 
 6 # 计算时间
 7 def timer():
 8     start = time.time()  # 记录程序开始时间
 9     func()
10     endtime = time.time() # 记录程序运行结束时间
11     print(endtime-start) # 打印程序运行时间
12 timer()

输出:

代码语言:javascript复制
程序正在运行......
5.00543737411499

好了,上面代码就是计算函数func的运行时间,大家是不是发现一个问题:只能计算func()函数的运行时间,那么如果我想计算别的函数的运行时间是不是就需要修改timer内部代码?(修改第9行,调用其他函数),还是很麻烦!再优化一下

代码语言:javascript复制
import time
def func():
    time.sleep(5)
    print('程序正在运行......')

# 计算时间
def timer(func):# 函数名字作为函数的参数
    start = time.time()  # 记录程序开始时间
    func()
    endtime = time.time() # 记录程序运行结束时间
    print(endtime-start) # 打印程序运行时间
timer(func)

输出:

代码语言:javascript复制
程序正在运行......
5.000637531280518

把目标函数的名字传递给timer()是不是就实现了计算任意函数运行时间的目的。只要把目标函数传递给timmer即可。ps:函数名字作为函数的参数怎么理解,自行百度一下-.-!

问题:

  问题又来了,如果我们要把这个函数丢给别人使用,别人就要在自己的代码中调用timmer(func)(成百的函数都要计算时间),别人也不愿意。设想一下如果我们想计算我们自己的任意一个函数(如func()函数)的运行时间,只要们直接调用函数本身就自动计算出这个函数的运行时间,是不是就很cool了。

像这样:计算func()函数的运行时间,只需要写func()然后输出执行时间

代码语言:javascript复制
 1 import time
 2 def func():
 3     time.sleep(5)
 4     print('程序正在运行......')
 5 
 6 # 调用函数
 7 def timer(func):# 函数名字作为函数的参数
 8     start = time.time()  # 记录程序开始时间
 9     func()
10     endtime = time.time() # 记录程序运行结束时间
11     print(endtime-start) # 打印程序运行时间
12 func = timer(func)
13 func() # 直接调用函数本身

是不是就很方便?那么该如何实现呢?下面接着看代码

代码语言:javascript复制
 1 import time
 2 def func():
 3     time.sleep(5)
 4     print('程序正在运行......')
 5 
 6 # 调用函数
 7 def timer(func):# 函数名字作为函数的参数
 8     def inner():
 9         start = time.time()  # 记录程序开始时间
10         func()
11         endtime = time.time() # 记录程序运行结束时间
12         print(endtime-start) # 打印程序运行时间
13     return inner 
14 func = timer(func)
15 func() # 直接调用函数本身

我们在timer函数内部添加一个函数用例包裹我们的程序代码,再在timer返回inner函数,就实现了我们的目的,不信?自己运行试试。 其实上面的timer就已经是一个装饰器了。

其实timer函数只是起到了修饰func函数的作用并给自己附加了一个功能,我们再分析一下上面代码的执行过程或原理

红线圈出来的部分其实是一个闭包。且外部函数返回内部函数的函数名字,这样我们就可以使用源函数的函数名字接受这个返回值然后执行inner内部的代码了

再看一下执行过程:

好了,感觉...貌似....好像....还是有点小缺陷,我们在计算任意函数的运行时间时,都要这样:func1 = timer(func1)....func1(),func2(),func3().......funcn(),还是很不理想

并不用担心,其实python提供了一种方法,叫做‘语法糖’,比如这样:

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner():
 6         start = time.time()  # 记录程序开始时间
 7         func()
 8         endtime = time.time() # 记录程序运行结束时间
 9         print(endtime-start) # 打印程序运行时间
10     return inner
11 @timer # 类似 func = timer(func)
12 def func():# 被装饰函数
13     time.sleep(5)
14     print('程序正在运行......')
15 
16 func()# 非被装饰函数,相当于调用inner函数

这样就方便了,可以在你想要计算运行时间的任意函数前添加@timer了。自己动手试试吧

上面我们介绍的是不带参数的装饰器,那么带返回值的装饰器又怎么实现呢?

二.带返回值的装饰器

看下面的代码思考:这样写是否正确

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner():
 6         start = time.time()  # 记录程序开始时间
 7         func()
 8         endtime = time.time() # 记录程序运行结束时间
 9         print(endtime-start) # 打印程序运行时间
10     return inner
11 @timer
12 # 被装饰函数
13 def func():
14     time.sleep(5)
15     print('程序正在运行......')
16     return '带返回值的装饰器'
17 str = func()
18 print(str)

我们先分析一下输出结果是什么?会不会输出“带返回值的装饰器”这个字符串?

输出:

代码语言:javascript复制
程序正在运行......
5.000662088394165
None

很显然并没有输出我们想要的结果,why?

因为函数加了装饰器之后们在调用的时候其实已经不是直接的调用函数的本身,而是调用装饰器中的inner函数来间接的调用被装饰函数,由于inner函数内部是没有返回值的,所以会输出none,修改代码

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner():
 6         start = time.time()  # 记录程序开始时间
 7         str = func()
 8         endtime = time.time() # 记录程序运行结束时间
 9         print(endtime-start) # 打印程序运行时间
10         return str
11     return inner
12 @timer
13 # 被装饰函数
14 def func():
15     time.sleep(5)
16     print('程序正在运行......')
17     return '带返回值的装饰器'
18 str = func()
19 print(str)

输出:

代码语言:javascript复制
程序正在运行......
5.0006444454193115
带返回值的装饰器

三.被装饰函数带参数

1.单个参数

当我们的被装饰函数是有参数的时候,我们又该如何修改我们的装饰器呢?

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner(a):
 6         start = time.time()  # 记录程序开始时间
 7         str = func(a)
 8         endtime = time.time() # 记录程序运行结束时间
 9         print(endtime-start) # 打印程序运行时间
10         return str
11     return inner
12 @timer
13 # 被装饰函数
14 def func(a):
15     time.sleep(5)
16     print('程序正在运行......' a)
17     return '带返回值的装饰器'
18 str = func('ing')
19 print(str)

分析:

被装饰函数func(a) 在调用时需要传参数a,那么应该由调用的地方传入参数,就是inner内部第7行代码,那么第7行的代码的参数由哪里来呢?应该由调用func(a)函数的函数inner传入,所以我们应该再inner函数传入一个参数a

输出:

代码语言:javascript复制
程序正在运行......ing
5.000949859619141
带返回值的装饰器
2.多个参数

上面修改过的装饰器只能使用在带一个参数的函数上,那么当我们需要在2个参数的被装饰函数上应该如何修改呢? 看下面代码

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner(a, b):
 6         start = time.time()  # 记录程序开始时间
 7         str = func(a, b)
 8         endtime = time.time() # 记录程序运行结束时间
 9         print(endtime-start) # 打印程序运行时间
10         return str
11     return inner
12 @timer
13 # 被装饰函数
14 def func(a, b):
15     time.sleep(5)
16     print('程序正在运行......' a)
17     print('程序仍在运行......'   b)
18     return '带返回值的装饰器'
19 str = func('ing', 'ing')
20 print(str)

输出:

代码语言:javascript复制
程序正在运行......ing
程序仍在运行......ing
5.000631809234619
带返回值的装饰器
3.万能参数装饰器

这样我们就可以把上面这个装饰器应用在带2个参数的函数上了,那么问题来了,如果带3个参数呢或者更多呢,哇!头都大了,那是不是要写好多个一样功能但是参数个数不同的函数了? 答案是否定的 看代码

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner(*args, **kwargs):
 6         start = time.time()  # 记录程序开始时间
 7         str = func(*args, **kwargs)
 8         endtime = time.time() # 记录程序运行结束时间
 9         print(endtime-start) # 打印程序运行时间
10         return str
11     return inner
12 @timer
13 # 被装饰函数
14 def func1(a):
15     time.sleep(5)
16     print('程序正在运行......' a)
17     print('程序仍在运行......' )
18     return '带返回值的装饰器'
19 @timer
20 # 被装饰函数
21 def func2(a,b):
22     time.sleep(5)
23     print('程序正在运行......' a)
24     print('程序仍在运行......'  b)
25     return '带返回值的装饰器'
26 
27 str = func1('ing')
28 print(str)
29 print('----------------------------')
30 str = func2('ing','ing')
31 print(str)

输出:

代码语言:javascript复制
程序正在运行......ing
程序仍在运行......
5.000674724578857
带返回值的装饰器
----------------------------
程序正在运行......ing
程序仍在运行......ing
5.000207424163818
带返回值的装饰器

是不是挺神奇的,自己动手试试看,是不是可以传任意的参数

ps:这里涉及到参数传递的知识,*args 和**kwargs 代表什么意思? 我这里就简单说一下,详细了解的话自己百度一下把

*args: 代表的是一个元祖,传参时按位置传递

**kwargs : 代表的是一个字典,传参数关键字传递

4.固定装饰器
代码语言:javascript复制
1 def timer(func):# 函数名字作为函数的参数
2     def inner(*args, **kwargs):
3          # 执行函数前做的
4         str = func(*args, **kwargs)
5          # 执行函数后做的
6         return str
7     return inner    

四.带参数装饰器

现在想一个问题,如果我们有很多个函数已经装饰了timer这个装饰器,那么我又不想要这个功能了,那我们该怎么办呢? 一个一个的删除装饰器嘛?超级麻烦

代码语言:javascript复制
 1 import time
 2 def outer(flag):
 3     # 装饰函数
 4     def timer(func):# 函数名字作为装饰器的参数
 5         def inner(*args, **kwargs):
 6             if flag:
 7                 start = time.time()  # 记录程序开始时间
 8                 str = func(*args, **kwargs) # 被装饰函数
 9                 endtime = time.time() # 记录程序运行结束时间
10                 print(endtime-start) # 打印程序运行时间
11             else:
12                 str = func(*args, **kwargs)
13             return str
14         return inner
15     return timer
16 
17 # 被装饰函数
18 @outer(True) # 带参数的装饰器
19 def func1(a):
20     time.sleep(1)
21     print('程序正在运行......' a)
22     print('程序仍在运行......' )
23     return 'b'
24 ret = func1('ing')
25 print(ret)

这样我们不想使用装饰器的时候只要传入一个False即可

看下输出

启用装饰器:

代码语言:javascript复制
程序正在运行......ing
程序仍在运行......
1.0000784397125244
b

屏蔽装饰器:

代码语言:javascript复制
程序正在运行......ing
程序仍在运行......
b

 有点晕吗?我们分析一下现在@outer(True) 代表的是什么意思?现在的@outer(True) 其实是分为两个部分的

1.@符号

2.outer(True): 表示纯纯的调用outer这个函数,因为outer函数返回了装饰器函数timer的名字,所以现在应该是这样的:timer = outer(True)

然后我们在连接@符号就编程 @timer 是不是就和之前一样了-> func = timer(func)

这样我们之前提到的问题是不是就轻松解决了呢

继续看一段代码

代码语言:javascript复制
1 def func():
2     print('这是一个简单的函数')
3 print(func.__name__) # 获取函数的名称
输出结果:
func

现在想一个问题,我们上面写好的装饰器,我要想在函数外部获取被装饰函数的函数名字也这么写会是什么情况?

代码语言:javascript复制
 1 import time
 2 
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     def inner(*args, **kwargs):
 6         print(*args)
 7         start = time.time()  # 记录程序开始时间
 8         str = func(*args, **kwargs)
 9         endtime = time.time() # 记录程序运行结束时间
10         print(endtime-start) # 打印程序运行时间
11         return str
12     return inner
13 @timer
14 # 被装饰函数
15 def func1(a):
16     #time.sleep(5)
17     print('程序正在运行......' a)
18     print('程序仍在运行......' )
19     return '带返回值的装饰器'
20 
21 
22 str = func1('ing')
23 print(str)
24 print(func1.__name__)

想一下,最后一行代码会不会输出:func1:

输出:

代码语言:javascript复制
ing
程序正在运行......ing
程序仍在运行......
0.0
带返回值的装饰器
inner

其实输出的是inner函数名(不理解的可以往前看,前面说过),那么我还是想输出func1的名字,该怎么实现呢?其实python自带了一个装饰器可以很好的解决这个问题

代码语言:javascript复制
 1 import time
 2 from functools import wraps
 3 # 装饰函数
 4 def timer(func):# 函数名字作为函数的参数
 5     @wraps(func)
 6     def inner(*args, **kwargs):
 7         print(*args)
 8         start = time.time()  # 记录程序开始时间
 9         str = func(*args, **kwargs)
10         endtime = time.time() # 记录程序运行结束时间
11         print(endtime-start) # 打印程序运行时间
12         return str
13     return inner
14 @timer
15 # 被装饰函数
16 def func1(a):
17     #time.sleep(5)
18     print('程序正在运行......' a)
19     print('程序仍在运行......' )
20     return '带返回值的装饰器'
21 str = func1('ing')
22 print(str)
23 print(func1.__name__)
输出:
ing
程序正在运行......ing
程序仍在运行......
0.0
带返回值的装饰器
func1

wraps(func) 装饰器的作用就是不改变被装饰函数任何作用

五.总结:

  一.什么是装饰器

    装饰器本质上就是一个python函数,他可以让其他函数在不需要做任何代码变动的前提下,增加额外的功能,装饰器的返回值也是一个函数对象。

  二.何时使用装饰器

    不想修改函数的调用方式,但是还想在原来的函数前后添加功能(实际公司项目中如果项目已经完成,但是不想再修改原代码,我们就可以使用装饰器)

  三.原则

    开放封闭原则

  1.开放原则:对扩展是开放的

    为什么要对扩展开放呢?

      我们说,任何一个程序,不可能在设计之初就已经想好了所有的功能并且未来不做任何更新和修改。所以我们必须允许代码扩展、添加新功能。

  2.对修改是封闭的

    为什么要对修改封闭呢?

      就像我们刚刚提到的,因为我们写的一个函数,很有可能已经交付给其他人使用了,如果这个时候我们对其进行了修改,很有可能影响其他已经在使用该函数的用户。

    装饰器完美的遵循了这个开放封闭原则。

0 人点赞