首先通过下面的代码引出今天的话题:
import time
def print_odds():
'''
输出0~100之间的所有奇数,并统计执行时间
'''
start_time = time.clock()# 起始时间
# 查找并输出所有奇数
for i in range(100):
if i % 2 ==1:
print(i)
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
if __name__ == '__main__':
print_odds()
这段代码的作用是输出0~100之间的所有奇数,并统计执行时间。但是其有一个很大的缺点,就是主要函数逻辑(查找奇数)和辅助功能(记录时间)耦合在一起,不方便修改,容易引起bug,那么我们能不能将辅助功能从主要功能函数中抽离出来?如是有了下面的修正:
import time
def count_time(func):
'''
统计某个函数的运行时间
'''
start_time = time.clock()# 起始时间
func()# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
def print_odds():
'''
输出0~100之间的所有奇数
'''
# 查找并输出所有奇数
for i in range(100):
if i % 2 ==1:
print(i)
if __name__ == '__main__':
count_time(print_odds)
上面的代码中,我们将辅助功能(记录时间)抽离出成为一个辅助函数count_time,在count_time中调用主要函数print_odds,这样做的优点是实现了解耦,使函数职责分离。但是还是有一个不符合逻辑的缺点,就是必须通过辅助函数来调用主要功能函数,这显然不合理,我们应该想办法通过调用主要功能函数来自动实现时间记录,而不是反过来。如是再次更改:
import time
def print_odds():
'''
输出0~100之间的所有奇数
'''
# 查找并输出所有奇数
for i in range(100):
if i % 2 ==1:
print(i)
def count_time_wrapper(func):
'''
闭包,用于增强函数func:给func增加统计时间的功能
'''
def improved_func():
start_time = time.clock()# 起始时间
func()# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
return improved_func
if __name__ == '__main__':
# 调用count_time_wrapper增强函数
print_odds = count_time_wrapper(print_odds)
print_odds()
上面的代码中,我们通过一个闭包来增强主要功能函数print_odds,给它增加一个统计时间功能。
闭包函数:是一个函数,其参数和返回值都是函数
用于增强函数功能
面向切面编程(AOP)
通过增强后,我们就可以通过调用主要函数来“顺带”实现辅助功能。但是我们还是发现了一个问题,就是我们需要显示进行闭包增强,即通过print_odds = count_time_wrapper(print_odds)
来实现增强,这样显得代码还是比较冗余。那么我们有什么办法“偷偷”地在调用主要函数时自动实现函数增强吗?装饰器顺应而生。
import time
def count_time_wrapper(func):
'''
闭包,用于增强函数func:给func增加统计时间的功能
'''
def improved_func():
start_time = time.clock()# 起始时间
func()# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
return improved_func
@count_time_wrapper
def print_odds():
'''
输出0~100之间的所有奇数
'''
# 查找并输出所有奇数
for i in range(100):
if i % 2 ==1:
print(i)
if __name__ == '__main__':
# 装饰器等价于在第一次调用函数时执行以下数据
# print_odds = count_time_wrapper(print_odds)
print_odds()
通过装饰器进行函数增强,只是一种语法糖,本质上跟上个程序的闭包增强完全一致,只是在代码上显得更加可观。
语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用
语法糖没有增加新功能,只是一种更方便的写法
语法糖可以完全等价地转换为原本非语法糖的代码
装饰器在第一次调用时被装饰函数增强
增强时机?第一次调用前
增强次数?只增强一次,一次增强后可多次使用
装饰器的原理并不复杂,但是有两个问题必须注意。首先是返回值问题,我们看下面的代码:
import time
def count_time_wrapper(func):
'''
闭包,用于增强函数func:给func增加统计时间的功能
'''
def improved_func():
start_time = time.clock()# 起始时间
func()# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
return improved_func
def count_odds(lim=100):
'''
统计0~lim之间所有奇数个数
'''
cnt = 0
for i in range(lim):
if i % 2 == 1:
cnt += 1
return cnt
if __name__ == "__main__":
print('增强前:')
print(count_odds())# 装饰前函数能正常返回,能接收参数
print('------------------------')
print('增强后:')
count_odds = count_time_wrapper(count_odds)
print(count_odds())
这段程序的主函数是统计0~lim(参数)之间所有奇数个数,装饰函数还是统计运行时间。与上面那个打印奇数的案例不同的是,这次的主函数有一个返回值cnt。我们先看一下运行结果:
增强前:
50
------------------------
增强后:
it takes 7.6e-06s to find all the olds
None
主要函数确实得到了装饰,但是那个返回值好像并没有返回出来,为什么?查看一下代码就知道了,我们在装饰时调用了一次主函数func()# 执行函数
,它本身是有一个返回值的,但是我们没有将他保存下来,更没有返回。对improved_func()
稍加修改:
import time
def count_time_wrapper(func):
'''
闭包,用于增强函数func:给func增加统计时间的功能
'''
def improved_func():
start_time = time.clock()# 起始时间
ret = func()# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
return ret # 增强函数的返回值应该是主要功能函数的返回值
return improved_func
def count_odds(lim=100):
'''
统计0~lim之间所有奇数个数
'''
cnt = 0
for i in range(lim):
if i % 2 == 1:
cnt += 1
return cnt
if __name__ == "__main__":
print('增强前:')
print(count_odds())# 装饰前函数能正常返回,能接收参数
print('------------------------')
print('增强后:')
count_odds = count_time_wrapper(count_odds)
print(count_odds())
修改后运行结果如下,成功获得了返回值:
增强前:
50
------------------------
增强后:
it takes 8.7e-06s to find all the olds
50
其次是含参问题,还是上面那个程序,我们可以看到在定义主函数时是有一个参数lim的,它默认为100,我们可以在传参时给他赋其他值,但是会由此引发一个问题,我们先看下列代码:
import time
def count_time_wrapper(func):
'''
闭包,用于增强函数func:给func增加统计时间的功能
'''
def improved_func():
start_time = time.clock()# 起始时间
ret = func()# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
return ret # 增强函数的返回值应该是主要功能函数的返回值
return improved_func
def count_odds(lim=100):
'''
统计0~lim之间所有奇数个数
'''
cnt = 0
for i in range(lim):
if i % 2 == 1:
cnt += 1
return cnt
if __name__ == "__main__":
print('增强前:')
print(count_odds(lim=1000))# 装饰前函数能正常返回,能接收参数
print('------------------------')
print('增强后:')
count_odds = count_time_wrapper(count_odds)
print(count_odds(lim=1000))
它的执行结果是增强后的函数报错了,错误如下:
TypeError: improved_func() got an unexpected keyword argument 'lim'
系统说improved_func()
函数接收了一个不曾期待的参数lim,但是我们不禁思考为什么说是improved_func()
接收到了这个lim参数,我们不是把它传给count_odds()
了吗?我们不妨修改修改一下if __name__ == "__main__":
下的内容:
if __name__ == "__main__":
print('增强前:')
print(count_odds.__name__)
print(count_odds(lim=1000))# 装饰前函数能正常返回,能接收参数
print('------------------------')
print('增强后:')
count_odds = count_time_wrapper(count_odds)
print(count_odds.__name__)
print(count_odds(lim=1000))
看一下程序运行结果(报错前的部分):
# import time...
增强前:
count_odds
500
------------------------
增强后:
improved_func
原来增强后的函数看似还是叫count_odds
,但是其本质已经是improved_func
了,这就解释得通为什么报错说improved_func()
函数接收了参数lim,而我们本身定义的函数improved_func()
并没有接收任何参数的能力,如是给它加上万能参数,问题解决。代码如下:
import time
def count_time_wrapper(func):
'''
闭包,用于增强函数func:给func增加统计时间的功能
'''
def improved_func(*args, **kwargs):
start_time = time.clock()# 起始时间
ret = func(*args,**kwargs)# 执行函数
end_time = time.clock()# 结束时间
print("it takes {}s to find all the olds".format(end_time - start_time))
return ret # 增强函数的返回值应该是主要功能函数的返回值
return improved_func
def count_odds(lim=100):
'''
统计0~lim之间所有奇数个数
'''
cnt = 0
for i in range(lim):
if i % 2 == 1:
cnt += 1
return cnt
if __name__ == "__main__":
print('增强前:')
print(count_odds(lim=1000))# 装饰前函数能正常返回,能接收参数
print('------------------------')
print('增强后:')
count_odds = count_time_wrapper(count_odds)
print(count_odds(lim=1000))
运行结果如下:
# import time...
增强前:
500
------------------------
增强后:
it takes 0.0001065s to find all the olds
500
这就是装饰器必须注意的两个问题,上面的代码为了方便解释采用的闭包形式,装饰器的原理是一样的。
下面通过一个面试题简单解释一下多层装饰器的使用:
def wrapper1(func1):
print('set func1')# 在wrapper1装饰函数时输出
def improved_func1():
print('call func1')# 在wrapper1装饰过的函数被调用时输出
func1()
return improved_func1
def wrapper2(func2):
print('set func2')# 在wrapper2装饰函数时输出
def improved_func2():
print('call func2')# 在wrapper2装饰过的函数被调用时输出
func2()
return improved_func2
@wrapper1
@wrapper2
def original_func():
pass
if __name__ == '__main__':
original_func()
这是一个两层的装饰器,我们先看一下运行结果:
set func2
set func1
call func1
call func2
为什么它是先装饰2再装饰1,而又先调用1再调用2?我们对下面三行代码进行解释:
@wrapper1
@wrapper2
def original_func():
等价于original_func = wrapper1(wrapper2(original_func))
original_func = wrapper1(wrapper2(original_func))又可以分解成
- 1.original_func = wrapper2(original_func)
- 2.original_func = wrapper1(original_func)
我们将if __name__ == '__main__':
的内容进行修改:
if __name__ == '__main__':
original_func = wrapper2(original_func)
print(original_func.__name__)
original_func = wrapper1(original_func)
print(original_func.__name__)
original_func()
查看一下执行结果:
set func2 # 1.装饰2前输出set func2
improved_func2 # 2.装饰2后函数变为improved_func2
set func1 # 3.装饰1前输出set func1
improved_func1 # 4.装饰1后函数变为improved_func1
call func1 # 5.装饰1在外层,先调用
call func2 # 6.装饰2在内层,后调用
也就是说装饰器是一个穿衣脱衣的过程,后穿上的衣服在外面,先脱;先穿的衣服在里面,后脱。
我们将面试题的if __name__ == '__main__':
的内容稍加修改,会得到一个很重要的结论,代码如下:
if __name__ == '__main__':
original_func()
print('----------------')
original_func()
运行一下:
set func2
set func1
call func1
call func2
----------------
call func1
call func2
由此可知,装饰器只在第一次被调用时被装饰函数增强,这一点非常重要。