在开始说装饰器之前,需要大家熟悉之前说过的相关知识点:
我们将通过对一个功能需求的分析和解决过程来探究一下“装饰器是什么”以及“装饰器的一些特性”。
假设我现在已经定义了一些函数,并且这些函数都已经被线上业务广泛应用。
现在线上某个业务响应时间过长,需要在不影响线上服务的情况下分别统计这些函数的运行时间来定位故障。
需求解读:
既然不能影响线上业务,那么必然是不能要求函数调用方去更改代码的,这当然包括调用方式。
实现方式:
函数调用:
输出结果:
存在的问题:
功能是实现了,但是存在以下几个问题:
改进思路:
要避免重复劳动,提高代码重用性,大家很自然就会想到把这个功能封装成一个函数。
函数调用:
输出结果:
存在的问题:
统计代码的重复性工作解决了,但是新的问题出现了:此时只能通过print_run_time(f)函数去调用原来的函数了(func1, func2, ...),这显然已经改变了函数的调用方式,因此是不合理的。
看上去越改越差劲了,我们继续分析下,会峰回路转的。
改进思路:
如果函数调用方式不能修改,那么我们只能给原来的函数名重新赋值一个新的函数体了,这个新的函数体就应该是添加完统计功能之后的函数体。我们看看下面这两种实现行不行:
print_run_time(f)是有参数的,而原函数func1和func2是没参数的,调用方式还是发生改变了,因此这种方式不可行。
上面这种方式显然更不行了,因为print_run_time(f)返回的是原函数的返回值,而这个返回值不是一个函数,这将导致被重新赋值后的func1和func2无法被调用。
那么我们是否可以把print_run_time(f)函数的函数体定义为一个内部的嵌套函数,然后将这个内部的嵌套函数作为print_run_time(f)函数的返回值呢?这貌似是说的通的,看下面的实现。
函数调用:
输出结果:
Cool! We got it! 我们现在所需要的做的是在所有需要统计运行时长的函数定义之后的任意地方执行一下下面这条语句就可以了:
存在的问题:
我们貌似忽略了一个问题,如果原函数有参数怎么办?
改进思路:
是的,我们定义print_run_time(f)函数的内部函数inner()时,为它定义相应的参数就可以了。由于每个函数的参数数量是不同的,因此inner函??的参数应该是可变(长)参数。
函数调用:
输出结果:
到目前为止:统计函数运行时间的功能实现了,函数原来的调用方式没有发生改变,原函数的定义也没有发生改变。其实这就是是装饰器的雏形。
装饰器,是一种“语法糖”,其本质上就是个函数。
它是一个装饰其他函数的函数,用来为其他函数添加一些额外的功能。
装饰器对被装饰的函数应该是完全透明的,即
高阶函数 + 嵌套函数 => 装饰器
这里的高阶函数需要同时满足以下两个条件:
再来看下上面写的实现代码:
对于这段代码来讲,print_run_time(f)就已经是一个装饰器函数。为了不改变原函数的调用方式,我们需要把print_run_time(f)函数的返回值重新赋值给原来的函数名:
但是我们上面说过,装饰器除了是一个函数之外,还是一个“语法糖”。“语法糖”应该是可以简化某些操作的,事实上确实是这样。上面的过程其实可以这样来写:
是的,就是这么简单。
就相当于:
不要问为什么,因为Python解释器就是这样执行的。但是需要注意,此时print_run_time(f)函数必须定义在被修改的函数定义之前,这个很容易理解,只要捋一下代码执行过程就明白了。另外,我们把print_run_time的内部函数名改成了wrapper,这个是装饰器函数的惯用名称(当然,也可以继续使用inner或使用任意名称)。
那么,现在我们可以像原来那样调用函数了(上面的修改这对函数的调用方是完全无感知的):
输出结果:
上面的“情景模拟”部分对于print_run_time函数的改进其实还没有完成。出于内容衔接和便于对概念理解的目的,才将这个改进放到了这里。现在我们返回来,看下这个print_run_time(f)函数还有什么不足。
现在要求在完成统计函数运行时间的基础上,如果函数运行时间超过指定的秒数则打印提示信息,且该秒数允许自定义。
存在的问题:
print_run_time(f)作为一个函数,目前对可而接收的参数限制太大--只能接受一个函数作为参数,要接收一个时间参数就必须为该函数定义新的参数。
解决思路:
这个看起来很容易解决,只要给print_run_time()函数定义一个用于接收超时时间的参数就可以了。
那么此时,我们可以这样来重新给func1和func2函数赋值并指定超时时间:
函数调用:
输出结果:
可以看到,我们设置的超时时间为2秒,func1函数的运行时间为1秒多,小于2秒,因此没有输出错误信息;而func2函数的运行时间为2秒多,大于2秒,因此输出了一个PROBLEM错误信息。Great! It works!
来看下这时候“装饰器语法糖”能正常工作吗?
因为print_run_time(f, timeout=1)的两个参数都是位置参数,要指定timeout的值就要先指定f的值,因此写法是这样的:
很抱歉,这种方式行不通,会报NameError: name 'func1' is not defined
。
解决思路:
既然f和timeout不能同时存在,但是又必须都存在,那我们就只能在print_run_time函数中定义一个内部函数来把它们分开了。也就是说print_run_time函数需要返回一个定义在它内部的函数,且这个内部函数需要满足一个正常的装饰器函数。那么参数f必然要定义在这个要被返回内部函数中,而参数timeout只能定义在print_run_time()这个外部函数中了。此时print_run_time函数的定义应该是这样的:
此时的print_run_time已经不再是一个装饰器函数,而是一个返回装饰器函数的函数。func1使用这个函数语法糖应该是这样的:
这个过程是这样的:
函???调用:
输出结果:
由于timeout是个默认参数(默认值为1),因此也可以不给它传递值,但是那对小括号不能省略:
此时的输出结果中就会多打印一条错误信息:
看上去,上面已经把所有需求都实现了。不要担心,还差一小步而已。此时我们可以打印一下被装饰函数的__name__属性看下:
输出结果:
发现函数func1和func2的名字都变成了wrapper,当然其实还有一些其他属性都变成了print_run_name的内部函数wrapper的属性值。这显然是不合适的,比如一些依赖函数签名的代码就可能会出现错误。解决这个问题也很简单,就是给print_run_name的内部函数warpper应用一个装饰器functools.wrapper就可以了(关于该装饰器的说明请自行翻阅python文档的funcstools模块):
此时再打印func1和func2的__name__属性值看看:
输出结果:
是的,这次没问题了。
关于Python的装饰器就先说到这里,希望对大家有所帮助。