python学习:函数---装饰器
一、什么是装饰器
现在有一个需求,年终考核的时候需要测试某个函数的执行效率,我们怎么做呢?比如这个函数叫func。
对于绝大多数人来说,第一印象肯定是如下的实现方法:
import time def func(): start = time.time() time.sleep(1) # 模拟程序延迟1秒 print(f'恭喜发大财!!!') end = time.time() print(end - start) func()
上述的解决办法是直接在func函数的开始和结尾添加代码来计算这个函数的执行时间。对于这个函数我们已经能计算出其执行时间,需求已经实现了。测试完成之后,我们再删除计算时间的代码就ok了。
但是,如果我们现在还有更多的函数都要去计算其执行时间,我们该怎么办呢?如果还是这样做,我们就需要为了暂时解决这个需求而在每个函数内部添加代码计算执行时间,等到考核结束了我们又要逐个函数去删除增加的代码。效率太低了。
此时,我们可能会想到把计算执行时间的代码封装成一个函数,在函数中调用原来的函数,这样会少些很多代码,而且也不用修改原来函数的代码,经过改造后的代码如下:
import time def func(): time.sleep(1) # 模拟程序延迟1秒 print(f'恭喜发大财!!!') def func2(): time.sleep(1) # 模拟程序延迟1秒 print(f'忽如一夜春风来!!!') def timer(f): start = time.time() f() end = time.time() print(end - start) timer(func) timer(func2)
上述代码将计算时间的过程封装成函数timer后,只需将原来函数的名称传给timer,调用的地方统一改为调用timer函数就可以了,大大减少了重复代码的编写,一切似乎变得很完美。
但是,冥冥中总觉得有些地方不对劲,到底是哪里有问题呢?哦,原来是要调用func,func2的地方都要改成调用timer,这明显是不合理的,完全是釜底抽薪。现实中我们已经写好了一个函数,不可能说由于你为了满足某个临时的需求,需要将调用这个函数的地方,全部改成调用另外一个函数。这样处理还不如第一种方式。
所以,需要继续改进。之前学闭包的时候,记得有一个闭包的常用方式,如下:
def outer(): age = 18 def inner(): print(age) return inner f = outer() f()
在这里,return将内部函数名返回给了outer,在调用outer的时候获得了inner的内存地址,就可以直接用这个inner函数的内存地址在外面来调用inner。
如果我也能在内部函数调用原来的func函数,然后内部函数的内存地址返回给outer,这样我就可以在调用outer后把inner的内存地址赋给一个和func相同名称的变量,然后使用func()。这样就不会改变原来的func的调用方式,也不会改变原来func函数的内部代码,两全其美,这就是装饰器,timer就叫装饰器函数。如下:
import time def func(): time.sleep(1) # 模拟程序延迟1秒 print(f'恭喜发大财!!!') def timer(f): def inner(): start = time.time() f() end = time.time() print(end - start) return inner func = timer(func) func()
这段代码的执行过程如下:
这样似乎还有最后一个问题,就是在原来的函数调用的地方的前面必须写上func=timer(func),好像也挺麻烦,如果连这一句代码都不写就真正的完美了。
在python中使用@来代替func=timer(func)。如下:
import time def timer(f): def inner(): start = time.time() f() end = time.time() print(end - start) return inner @timer # func = timer(func) def func(): time.sleep(1) # 模拟程序延迟1秒 print(f'恭喜发大财!!!') func() # 这里的func()其实是inner()
二、装饰器的作用
在不改变原来函数的源代码以及调用方式的前提下,为函数添加新的功能。
三、原则:开放封闭原则
1、开放:对扩展是开放的
任何一个程序,不可能在设计之初就已经想好了所有的功能并且未来不做任何更新和修改。所以我们必须允许代码扩展、添加新功能 。
2、封闭:对修改是封闭的
对于已经交付给其他人调用的函数,如果我们修改了这个函数的源代码,就有可能印象其他已经在调用这个函数的用户。
四、装饰器的固定模式(要背下来)
import time def wrapper(f): def inner(*args, **kwargs): '''执行被装饰函数之前要做的操作''' ret = f(*args, **kwargs) '''执行被装饰函数之后要做的操作''' return ret return inner @wrapper # func = wrapper(func) def func(): time.sleep(1) # 模拟程序延迟1秒 print(f'恭喜发大财!!!') func()
示例:
五、带参数的装饰器
前面的装饰器只能接收一个参数,而且这个参数必须是传入被装饰函数的函数名。怎么传入其他的参数呢?实际应用中,有可能开始的时候是用某个装饰器的,但是随着需求的改变,后期不再使用这个装饰器了,那么就要逐个去注释掉使用的这个装饰器,很麻烦。如果能定义一个开关来控制是否使用这个装饰器就完美了,这只是一个伪需求,举个例子而已。
1 import time 2 3 def timer(flag): 4 def wrapper(func): 5 def inner(*args,**kwargs): 6 if flag: 7 start = time.time() 8 ret = func(*args,**kwargs) 9 end = time.time() 10 print(end - start) 11 return ret 12 else: 13 ret = func(*args, **kwargs) 14 return ret 15 return inner 16 return wrapper 17 18 @timer(True) 19 def shopping_add(): 20 time.sleep(0.1) # 模拟函数执行需要的时间 21 print("shopping_add...") 22 23 @timer(False) 24 def shopping_del(): 25 time.sleep(0.1) # 模拟函数执行需要的时间 26 print("shopping_del...") 27 28 shopping_add() 29 shopping_del() # 不会打印出函数的执行时间了
带参数的装饰器需要注意@timer(True),这里分两步理解:
第一步、先执行timer(True),这只是一个函数调用,调用后返回了wrapper
第二步、@与wrapper再组合成一个装饰器
六、多个装饰器装饰同一个函数
1 def wrapper1(func): 2 def inner1(*args,**kwargs): 3 print("wrapper1,before func") 4 func(*args,**kwargs) 5 print("wrapper1,after func") 6 return inner1 7 8 def wrapper2(func): 9 def inner2(*args,**kwargs): 10 print("wrapper2,before func") 11 func(*args,**kwargs) 12 print("wrapper2,after func") 13 return inner2 14 15 @wrapper2 16 @wrapper1 17 def f(): 18 print("f()...") 19 20 f()
执行过程图解:
七、wraps装饰器:from functools import wraps
首先,我们要打印一个函数的名称,如下代码:
1 def my_func(): 2 '''这是我的自定义函数''' 3 print(1) 4 5 print(my_func.__name__) # my_func 6 print(my_func.__doc__) # 这是我的自定义函数
如果这个函数被装饰器函数装饰后,使用上述方式就打印不出本来的函数名称了,此时__name__得到的是装饰器函数中的inner的名称,如下代码:
1 def wrapper(func): 2 def inner(*args,**kwargs): 3 func() 4 return inner 5 6 @wrapper 7 def my_func(): 8 '''这是我的自定义函数''' 9 print(1) 10 11 print(my_func.__name__) # inner 12 print(my_func.__doc__) # None
那怎么才能得到原始函数的名称呢,这个时候就要用到wraps装饰器了,如下代码:
1 from functools import wraps 2 3 def wrapper(func): 4 @wraps(func) 5 def inner(*args,**kwargs): 6 func() 7 return inner 8 9 @wrapper 10 def my_func(): 11 '''这是我的自定义函数''' 12 print(1) 13 14 print(my_func.__name__) # my_func 15 print(my_func.__doc__) # 这是我的自定义函数
八、作业
1、编写装饰器,为多个函数添加同一个装饰器。(如果登录成功,以后就不需要再输入用户名和密码了)
1 FLAG = False 2 def login(func): 3 def inner(*args,**kwargs): 4 global FLAG 5 if FLAG: 6 ret = func(*args, **kwargs) 7 return ret 8 else: 9 username = input("请输入用户名:") 10 password = input("请输入密码:") 11 if username == "zhangsan" and password == "123": 12 FLAG = True 13 ret = func(*args,**kwargs) 14 return ret 15 else: 16 print("登录失败:用户名或密码错误") 17 return inner 18 19 @login 20 def shopping_add(): 21 print("增加一件物品") 22 23 @login 24 def shopping_del(): 25 print("移除一件物品") 26 27 @login 28 def shopping_list(): 29 print("查看购物车") 30 31 shopping_add() 32 shopping_list() 33 shopping_del()
2、编写装饰器,为每个函数的调用添加日志
1 import time 2 def logger(func): 3 def inner(*args,**kwargs): 4 with open("log","a",encoding="utf-8") as f: 5 t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 6 f.write(t + " : " + func.__name__ + "\n") 7 ret = func(*args,**kwargs) 8 return ret 9 return inner 10 11 @logger 12 def shopping_add(): 13 print("增加一件物品") 14 15 @logger 16 def shopping_del(): 17 print("移除一件物品") 18 19 @logger 20 def shopping_list(): 21 print("查看购物车") 22 23 shopping_add() 24 shopping_del() 25 shopping_list()
3、下载网页内容,并将内容写入文件,如果网页内容已下载,就直接从文件读取。
1 import os 2 from urllib.request import urlopen 3 4 def cache(func): 5 def inner(*args,**kwargs): 6 if os.path.getsize("file"): 7 with open("file","rb") as f: 8 return f.read() 9 ret = func(*args,**kwargs) 10 with open("file","wb") as f: 11 f.write(ret) 12 return ret 13 return inner 14 15 @cache 16 def get(url): 17 content = urlopen(url).read() 18 return content 19 20 ret = get("http://www.baidu.com") 21 print(ret)
4、