41.Python编程:装饰器
前言
前面我们在学习如何《30行代码实现微信好友消息自动回复功能》时,留下了一个Python中非常重要的知识点:装饰器,今天我们就来详细学习Python中的装饰器。
当时是这样写的:在自定义一个函数时,用了@itchat.msg_register(itchat.content.TEXT)
进行修饰。如下:
# 接收好友发来的消息并自动回复
@itchat.msg_register(itchat.content.TEXT)
def auto_reply(msg_info):
# 此处省略若干...
要想彻底搞懂Python中的装饰器,除了需要有一点Python中的函数基础,还需要解决如下四个问题。当我们解决了这四个问题后,也就彻底搞懂Python中的装饰器。
1.什么是装饰器,其本质是什么
2.装饰器有什么作用?
3.装饰器有什么使用特点(使用原则)?
4.装饰器的应用场景
提示:如果你还不知道Python中的函数,请先了解函数后,再来学习。
装饰器
我们按照上面提的四个问题,通过寻找问题答案一一剖析这个装饰器。如下就是一个自定义的打印函数日志的装饰器,先来大概看下其定义样式。
# 打印日志的装饰器
def my_log(func):
def wrapper(*args, **kwargs):
print("print_log")
result = func(*args, **kwargs)
print("{}函数调用时刻:{}".format(func.__name__, datetime.datetime.now()))
return result
return wrapper
1.什么是装饰器,其本质是什么
Python中的装饰器并没有什么神秘的,其本质上就是个函数。可以用一个等式理解:
装饰器 = 高阶函数 + 嵌套函数
。
知道为什么一再提示如果你还不知道Python中的函数,请先了解函数后,再来学习装饰器了吧。
提示:
关于Python中,什么是函数、函数的本质、函数的参数等一系列内容,可参考本系列的《Python中的函数》。
补充:
1.函数就是一个对象,函数名是一个指向该对象的变量。
2.高阶函数:参数中有以函数为参数,或者返回值是函数的函数为高阶函数
3.函数嵌套:函数里面又定义了函数。
2.装饰器有什么作用?
装饰器,说白了就是用来增强其他函数功能的函数。其形式,通常是在被修饰函数定义时,用@装饰器名
修饰。
例如:
# 计算平方
@my_log
def cal_square(x):
result = x * x
print("{} * {} = {}".format(x, x, result))
return result
这里的,我自定义了一个计算一个数平方的函数cal_square
,要求调用这个函数时打印其调用日志,就可以写一个日志装饰器,假设命名为my_log函数
,用来装饰所要调用的函数。
装饰器的定义如上面。
3.装饰器有什么使用特点(使用原则)?
- 1.不会修改被装饰的函数的源代码。(对函数的源代码没有任何修改,只是在原来功能的基础上额外增强函数的功能。)
- 2.不会改变被装饰函数的调用方式。(原来怎么调用,被装饰后依旧怎么调用。)
- 3.不会改变被装饰函数的返回结果。
这三点使用特点非常重要,归结为:原来的函数之前怎么调用、怎么入参、什么样的返回值,使用装饰器装饰后这些都不受任何影响。也就是:我们在不对原来函数的源代码、调用形式、返回值等任何修改的情况下,增强了原来函数的功能。
4.装饰器的应用场景
应用场景非常多,开发中常见的如:
1.插入日志
2.性能测试
3.处理事务
4.开发python开源框架时,非常常用
示例
有了以上认识,我们看一个完整的示例:
- 需求1:
打印出一个函数调用的日志信息,比如被调用的函数名、调用时刻等。
方案一:修改函数源代码,我们可以在定义时就写入打印出一个函数调用的日志信息。【极不推荐】
不推荐理由:如果一个项目中,已有成千上万个函数、方法,累到吐血不说,还修改了原函数的源码。
方案二:使用装饰器。【推荐】
# 打印日志的装饰器
def my_log(func):
# 嵌套函数wrapper,接入参数 保证原函数返回结果不受任何影响
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print("{}函数调用时刻:{}".format(func.__name__, datetime.datetime.now()))
return result # 保证原函数返回结果不受任何影响
return wrapper # 返回嵌套函数wrapper,特别注意,不是返回wrapper()
# 计算平方
@my_log
def cal_square(x):
result = x * x
print("{} * {} = {}".format(x, x, result))
return result
# 调用函数,计算3的平方
cal_square(3)
运行结果,如果在定义函数def cal_square(x):
时,没有加装饰器@my_log
,则就是前面我们之前最常用的函数调用。现在我们用了装饰器,打印结果就有了日志。现在运行结果如下:
3 * 3 = 9
cal_square函数调用时刻:2018-08-18 19:26:24.660156
说明:
1.在定义装饰器时,入参传入了一个函数,所以装饰器是一个高阶函数,也返回了一个内层函数名。
2.特别提醒: 外层函数的返回结果是嵌套函数wrapper
函数名,而不是返回wrapper()
。这两个是不同的概念哦。
3.嵌套函数中的内层函数,函数名可以是任何符合Python命名规则的标识符。我这里命名wrapper
,你可以自定义为其他合法的函数名。
4.为了保证原函数的调用入参不受任何影响,内层函数入参:*args, **kwargs
。
5.为了保证原函数的返回值不受任何影响,我们用一个临时变量result
接收,并在内层函数进行返回了result
。
6.你还记得前面提到的装饰器的本质吗?
其本质上就是个函数。可以用一个等式理解:装饰器 = 高阶函数 + 嵌套函数
。
多个装饰器函数名问题
- 需求2:在打印函数调用日志基础上,打印出函数的运行耗时时间。
import time, datetime
import functools
# 装饰器:打印方法耗时
def my_time(func):
def wrapper(*args, **keywords):
start = time.time()
print("print_time")
result = func(*args, **keywords)
end = time.time()
t = end - start
print("{}方法执行耗时:{:.6}秒".format(func.__name__, t))
return result
return wrapper
# 打印日志的装饰器
def my_log(func):
def wrapper(*args, **kwargs): # 嵌套函数wrapper,接入参数 保证原函数返回结果不受任何影响
result = func(*args, **kwargs)
print("{}函数调用时刻:{}".format(func.__name__, datetime.datetime.now()))
return result # 保证原函数返回结果不受任何影响
return wrapper # 返回
# 计算平方
@my_time
@my_log
def cal_square(x):
time.sleep(1)
result = x * x
print("{} * {} = {}".format(x, x, result))
return result
# 调用函数,计算5的平方
cal_square(5)
为了让函数有耗时操作,我们在函数内time.sleep(1)
。调用函数,计算5的平方cal_square(5)
,运行结果:
5 * 5 = 25
cal_square函数调用时刻:2018-08-18 19:41:21.604226
wrapper方法执行耗时:1.00064秒
说明:
1.我们在函数cal_square
定义时,用了两个装饰器进行装饰,打印函数调用日志@my_log
、计算函数运行耗时@my_time
。
2.运行结果中,wrapper方法执行耗时:1.00064秒
,为什么是wrapper
方法呢?
这是因为我们用了两个装饰器,层层装饰过程中,func.__name__
先拿到的是传入的cal_square
,第二个装饰器拿到的是第一个装饰器的内层函数wrapper
的函数名。拿到这样的函数名,如果有些依赖函数签名的代码执行就会出错。
3.怎么样才能拿到正确的函数名呢?打印方法名都是:cal_square
。
用functools.wraps(func)
,不需要在装饰器函数中编写wrapper.__name__ = func.__name__
这样的代码,因为Python内置的functools.wraps
装饰器就是干这个事的,所以,一个完整的decorator
的写法如下:
# 装饰器:打印方法耗时
def my_time(func):
@functools.wraps(func)
def wrapper(*args, **keywords):
start = time.time()
result = func(*args, **keywords)
end = time.time()
t = end - start
print("{}方法执行耗时:{:.6}秒".format(func.__name__, t))
return result
return wrapper
两个装饰都这么修改后,打印结果如下:
5 * 5 = 25
cal_square方法执行耗时:1.0007秒
cal_square函数调用时刻:2018-08-18 19:59:05.342631
现在,只需在定义wrapper()
的前面加上@functools.wraps(func)
即可。此时,符合我们需求了。
装饰器进阶
- 需求3:
现要求可以自定义的打印日志。 - 方案:
这就需要在装饰器基础上,外面再嵌套一层函数,接收自定义传入的参数,即三层函数嵌套。代码如下:
# 装饰器进阶:可传入参数的装饰器
def my_log_text(txt):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(txt)
result = func(*args, **kwargs)
# print("调用时间:{}".format(datetime.datetime.now()))
return result
return wrapper
return decorator
# 计算平方
@my_log_text("装饰器高阶测试")
def cal_square(x):
time.sleep(1)
result = x * x
print("{} * {} = {}".format(x, x, result))
return result
# 调用函数,计算7的平方
cal_square(7)
运行结果:
装饰器高阶测试
7 * 7 = 49
调用时间:2018-08-18 20:58:50.039390
小结
本文学习Python中的装饰器,另外又学习了装饰器进阶知识:可以传入参数的装饰器、多装饰器中函数名的问题的处理等。学习此知识,除了需要有一点Python中的函数基础,还需要解决文中提到的四个问题。当我们解决了这四个问题后,也就彻底搞懂Python中的装饰器了。
装饰器思维导图截图如下:
装饰器笔记截图更多了解,可关注公众号:人人懂编程
微信公众号:人人懂编程