理解python装饰器有那么难么?

很多人学到python的装饰器之后,感受到泰山压顶般的困难,似乎这是一座不可逾越的高山。本文尝试用最简单明了的语言为你解释什么是装饰器。

1. 背景介绍

我们写了一些函数,实现了某个功能,最后执行代码时,发现代码执行速度很慢,于是,你想获得函数执行时长,找出比较慢的那一个,这就是我们讲解装饰的技术背景,下面是试验函数,为了模拟函数执行时长,我在函数里使用了sleep

import time

def slow_func1():
    time.sleep(0.1)   # 这个很快


def slow_func2():
    time.sleep(3)     # 假装很慢

def run():
    for i in range(20):
        slow_func2()
    slow_func1()

run()   # 执行程序

现在,你不知道这两个函数各自执行时长,脚本启动后,执行run方法,速度不理想,我们需要一个方法来知晓程序慢在哪里。

2. 直接测量计算

想要获取函数执行时长,这还不容易,函数执行之前,获取当前时间,函数执行结束以后,再获取当前时间,时间差就是函数的执行时长

t1 = time.time()
slow_func1()
t2 = time.time()
print("slow_func1执行时长是: ", t2-t1, '秒')

t1 = time.time()
slow_func2()
t2 = time.time()
print("slow_func2执行时长是: ", t2-t1, '秒')

程序输出结果

slow_func1执行时长是:  0.10361218452453613 秒
slow_func2执行时长是:  3.0028281211853027 秒

very good,这种方法可以解决我们所面临的问题,但是,它有缺陷,如果有20个函数都需要获取执行时长,该怎么办?一个函数就需要4行代码,20个函数就是80行代码,我是搞技术的,不是来被技术搞的,不行,我需要换一个思路。

3. 用函数解决

测量函数执行时长的代码,看起来长的都一样啊,我编写一个函数就不可以了么

def func_cost(func):
    t1 = time.time()
    func()
    t2 = time.time()
    print("{name}执行时长是: ".format(name=func.__name__), t2-t1, '秒')


func_cost(slow_func1)
func_cost(slow_func2)

我编写一个func_cost函数,它有一个参数,我将slow_func1,slow_func2作为参数传入到func_cost中,在func_cost函数中执行并计算时长,就可以达到相同的效果,程序输出结果是

slow_func1执行时长是:  0.10442304611206055 秒
slow_func2执行时长是:  3.0001602172851562 秒

函数也能作为参数是理解这一段代码的关键,如果你能理解这一段代码,那么恭喜你,你离掌握装饰器已经不远了,目前的这个方法可以解决我们的问题,函数func_cost用了5行代码,假设有20个函数需要测量,那么只需要再写20行调用func_cost函数的语句就可以了,但是,它也是有缺陷的。

假设,slow_func1函数在整个程序的执行期间要执行100次,我想知道每一次执行的时长,该怎么办?为了方便,我在定义slow_func1时没有定义参数,而实际工作中,一个有参数的函数由于参数的不同,执行时长也可能会不同。

现在这个解决办法,不能模拟出程序真实的运行过程,只能用func_cost函数把每一个待测量的函数执行一次,我们需要一种方法,可以在程序真实的运行过程中测量函数执行时长。

4. 设计一个复杂的函数

先上代码,再做解释

def func_cost(func):
    def wapper():
        t1 = time.time()
        func()
        t2 = time.time()
        print("{name}执行时长是: ".format(name=func.__name__), t2-t1, '秒')

    return wapper

slow_func1 = func_cost(slow_func1)
slow_func2 = func_cost(slow_func2)
run()   # 执行程序

这一次的func_cost函数更加复杂了一点,在func_cost内部,我定义了一个函数wapper, 先不关心wapper函数里面的东西,明确一点,函数func_cost的返回值是wapper,不必大惊小怪,函数既能做参数,也能做返回值。

func_cost(slow_func1) 在执行时,返回的是一个名为wapper的函数,在函数wapper内部,会测量slow_func1的执行时长,wapper函数最终赋值给变量slow_func1, run函数执行时,run函数里的slow_func1不再是之前用def定义的那个slow_func1,而是使用func_cost函数得到的wapper。

好吧,不论怎样言简意赅,装饰器都确实很难轻易说清楚,这就是你对装饰器理解的最后一道山岗,胜利就在前方。这个方法解决了前面出现过的所有问题,但是,它还是有缺陷,为了测量函数的执行时长,这个方法实际上是用func_cost创建出一个新的函数然后替代之前用def定义的函数,测量结束后,再删掉这些代码么,有没有什么更加简便的写法?

5 使用@

先上代码,再做解释

import time

def func_cost(func):
    def wapper():
        t1 = time.time()
        func()
        t2 = time.time()
        print("{name}执行时长是: ".format(name=func.__name__), t2-t1, '秒')

    return wapper

@func_cost
def slow_func1():
    time.sleep(0.1)   # 这个很快

@func_cost
def slow_func2():
    time.sleep(3)     # 假装很慢

def run():
    for i in range(20):
        slow_func1()
    slow_func2()

run()   # 执行程序

在需要测量执行时长的函数上方加上一行代码@func_cost

@func_cost
def slow_func1():

它的作用等价于

slow_func1 = func_cost(slow_func1)

这种语法的好处在于,装饰器与函数紧挨着,想要去除装饰器效果时,只需要删除代码即可,而且,代码看起来也更加简练了,当然,你不懂原理,看起来也很懵逼。

6. 写在最后

本文从原理上为你讲解装饰器,一些更加核心的深层技术,我没有做讲解,以避免你为了学习一个知识点不得不学习更多的知识点。

本文最终编写的func_cost装饰器不能用于实际工作,因为它不是一个完善的装饰器,还不能装饰有参数的函数。如果你想更全面的学习装饰器,可以到装饰器章节 更深入的学习

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案