不久前我在参加某比赛时,写了一些处理数据的代码,当时并没有采用SQL,而是用Python去实现。为了计算监测处理数据所耗费的时间,当时写了这样code:
1 | import time |
这样写确实没什么问题,但是一旦使用的次数多了,会发现代码非常累赘,比如说要得到func1(),func2(),func3()三个函数的运行时间,就得写三次上面的代码。
那时我就在想,Python没有提供实现这种功能的内置函数吗?当然,因为只是一个小问题所以当时也没有去细究。直到前阵子,在写装饰器有关的代码时,想起了这件事,一拍脑袋,这不就是我要的“内置函数”嘛,有了装饰器,实现这个计时的小功能不是分分钟的事嘛。当然,后面我又碰到了难题,请往下看。
首先,我写了这么一个装饰器,用来获取函数运行的时间:1
2
3
4
5
6
7
8def runTime(func):
def wrapper(*args,**kwargs):
import time
t1 = time.time()
func(*args,**kwargs)
t2 = time.time()
print "%s run time: %.5f s" %(func.__name__,t2-t1)
return wrapper
然后我写了个冒泡排序来测试一下:
1 | @runTime |
一切都很顺利,程序输出:
1 | bubbleSort run time: 0.35800 s |
我又写了一个斐波那契数列来测试:1
2
3
4
5
6
7
8
9
10@runTime
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
fib(10)
发现输出了很多的错误信息:
1 | fib run time: 0.00000 s |
为什么会出现这种错误?
深入理解了装饰器就知道,这里fib = runTime(fib) = wrapper,而fib又是一个递归函数,所以当调用fib(10)的时候,其实执行的是:
1 | fib(10) -> fib(9)+fib(8) -> fib(8)+fib(7)+fib(7)+fib(6) ->..... |
那么程序首先肯定会执行一次fib(1)和fib(0),这时候是OK的,并且由于fib是被装饰的函数,所以fib(1)和fib(0)执行后,会打印出两行:1
2fib run time: 0.00000 s
fib run time: 0.00000 s
这没有任何问题,但是打印完后,问题就出来了。前面说过,fib等于wrapper,而wrapper函数里没有返回值,即返回值为NoneType,那这下好玩了:1
fib(1)+fib(0) = wrapper(1)+wrapper(0)=NoneType+NoneType
行文至此,所有问题都一目了然了吧。
所以,当你对一个递归函数做装饰的时候,不要忘了递归函数里的每一层调用,都是被装饰过的。这个时候如果你的程序没有问题,那谢天谢地。如果程序有问题,也不必慌张,对付递归函数,其实在函数外面再加个套就行了:
1 | @runTime |
这个时候仍然是fib = runTime(fib),只不过fib不再是递归函数,_fib才是,但装不装饰已不关_fib鸟事。