Python开发(人工智能/大数据/机器学习)Python

41.Python编程:装饰器

2018-08-18  本文已影响41人  TensorFlow开发者

前言

前面我们在学习如何《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.装饰器有什么使用特点(使用原则)?

4.装饰器的应用场景
应用场景非常多,开发中常见的如:

1.插入日志
2.性能测试
3.处理事务
4.开发python开源框架时,非常常用

示例

有了以上认识,我们看一个完整的示例:

方案一:修改函数源代码,我们可以在定义时就写入打印出一个函数调用的日志信息。【极不推荐】
不推荐理由:如果一个项目中,已有成千上万个函数、方法,累到吐血不说,还修改了原函数的源码。

方案二:使用装饰器。【推荐】

# 打印日志的装饰器
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.你还记得前面提到的装饰器的本质吗?
其本质上就是个函数。可以用一个等式理解:装饰器 = 高阶函数 + 嵌套函数

多个装饰器函数名问题

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)即可。此时,符合我们需求了。

装饰器进阶

# 装饰器进阶:可传入参数的装饰器
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中的装饰器了。

装饰器思维导图截图如下:

装饰器笔记截图

更多了解,可关注公众号:人人懂编程


微信公众号:人人懂编程
上一篇 下一篇

猜你喜欢

热点阅读