由于装饰器的结构和使用形式,相信很多python的初学者在学习的过程中有很多困惑,本文尽量站在初学者的角度,用大白话和简单的代码对装饰器进行讲解,绕开闭包和对象引用的概念,希望尽可能减少初学者在学习装饰器时的困惑。
1 什么是装饰器
其实对于初学者来说,最大的疑惑可能是装饰器是干什么用的?为什么我在编程的过程中基本上用不到,我在什么场合下必须用它呢?
其实装饰器很简单,从名字上就可以看出它的功能,它就是装饰用的。而它的装饰对象就是函数,而所谓的“装饰”就是给函数添加功能的意思。
这个时候你可能会想,给函数添加功能,直接修改函数不就行了,为啥要用装饰器呢。事实上确实是这样,用装饰器能完成的功能,直接修改函数也能达到同样的效果,这也是为什么初学者基本上用不到装饰器的原因。但是在实际的项目的某个阶段,你可能不允许改动测试好的函数,而想增加一些功能,比如统计这个函数运行的时间,那么这个时候,装饰器就派上用场了。这也恰恰是装饰器的本质所在,在不改变函数和它的引用的情况下,增加函数的功能。
那么基于以上的分析,假设现在我们有如下的函数,其作用是在两秒后,输出打印的内容。在这个函数的基础上,我们提出以下需求,后文将在解决这个需求的过程中,逐步深入装饰器的原理。
import time
def print_function():
time.sleep(2)
print("this is a print function")
print_function() #1
1、给该函数增加一个运行耗时统计的功能
2、不改动这个函数的内部代码
3、不改动这个函数的引用
好,如果你没有思考过这个问题,可以在这里停下来想一想再继续下面的内容。
2 问题的解决
现在我们想要增加一个统计这个函数运行时间的功能,而不改动函数的源码,首先我们想到的是,在这个函数引用(#1 处)的外部,添加这些计时功能的代码,就像下面这样:
import time
def print_function():
time.sleep(2)
print("this is a print function")
time_start=time.time()
print_function() #1
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
可以看到,通过在 #1处的上下增加代码,我们实现了函数的计数功能,而且没有改变函数的内部代码。那么这些增加的计时所用的代码就是所谓的“装饰”,其实装饰器的原理就是这么简单。但是这种方式增加的代码,让函数的引用变得更加复杂。现在我们换一种方式,让代码看起来更加优雅。很自然的一个想法是,我们把最后四行再封装成一个函数,去引用它。
import time
def print_function():
time.sleep(2)
print("this is a print function")
def print_function_time():
time_start=time.time()
print_function() #1
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
print_function_time()
很显然,print_function_time() 就是我们要的函数,但是由于它的函数名改变了,不符合我们一开始提出的问题。那有没有什么办法不改名函数名,也能实现同样的功能呢,答案就是高阶函数。其实不要被高阶函数这个名字唬住了,它和普通函数唯一的区别就在于它返回的不是数值、字符串、列表等这样普通的对象,它返回的是一个函数。我们知道,当我们使用下面这条语句之后,f() 和print_function() 其实是等价的。
f=print_function()
那么我们自然想到,把我们要“装饰”的函数(print_function)传到一个函数里面,增加计时功能之后,再把它当做返回值返回,废话不多说,上代码。
import time
def print_function():
time.sleep(2)
print("this is a print function")
def timer(func): #1
def deco():
time_start=time.time()
func() #2
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return deco
print_function=timer(print_function) #3
print_function()
#1 处我们定义了一个装饰函数,#2 处为传入的被装饰的函数,最外层的timer函数返回的是 deco函数,也就是“装饰后”的 func 函数。
因此我们在 #3处 引用 timer(print_function) 其实就是引用了deco(),也就是被装饰后的print_function 函数,最后一行中print_function()的引用虽然和之前定义的函数名相同,但实际上,他已经在原来的基础上增加了计时功能。
至此,我们已经实现了开头提出来的问题,在不改变函数内部代码和函数引用的情况下,增加计时功能。这就是一个装饰器的工作原理和过程,其中print_function 我们可以称之为被装饰的对象(函数),外部的 timer 就是装饰器,当函数被当成参数传入到装饰器中,装饰器返回一个被装饰之后的函数,这个过程我们可以称之为装饰。
当然python 提供了一种比较直观的装饰器语法,来代替上面这种朴素的写法,使代码更加优雅。
3 python 中装饰器的写法
在我们上面的写法中,需要在每个被装饰的函数前面加上一句
print_function=timer(print_function)
python 提供了一种更加简洁的写法。在需要装饰的函数定义(#1 处)上面加 @timer 语句,就表示该函数被timer函数装饰,注意,在引用装饰函数(@timer)之前,需要先定义装饰函数,不然会报错。下面这段代码是python 装饰器的标准写法,但是它和第2小节中的那段代码是等效的。
import time
def timer(func):
def deco():
time_start=time.time()
func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return deco
@timer # 1
def print_function():
time.sleep(2)
print("this is a print function")
print_function()
4 装饰有返回值的函数和带有参数的函数
相信看到这,你已经对装饰器的原理和作用有了一个初步的理解。在上面我们被装饰的函数是一个没有返回值和没有参数的函数,那么假如要装饰有返回值和有参数的函数,该怎么做呢,首先,我们来看有返回值的函数。
4.1 装饰有返回值的函数
装饰有返回值的函数其实很简单,在上面一段代码中,其实我们最后引用的print_function() 其实就是 deco()函数,显然目前deco 函数是没有返回值的,所以需要在deco函数中添加一个 return 项,那么要return 什么呢?因为我们要的是被装饰函数(print_function)的返回值,其实就是要返回其内部 func()的返回值,所以需要把func()的返回值记录下来,然后在deco 函数中返回:
import time
def timer(func):
def deco():
time_start=time.time()
value=func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return value
return deco
@timer # 1
def print_function():
time.sleep(2)
print("this is a print function")
return "run over"
print_function() #1 输出 'run over'
4.2 返回带有参数的函数
实际中的函数往往是有参数的,如果将上面被装饰的print_function 函数定义处直接改成有参函数,如下面的代码所示,显然这段代码是会报错的,因为在装饰函数中传入的函数是没有参数的函数。
import time
def timer(func):
def deco():
time_start=time.time()
value=func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return value
return deco
@timer
def print_function(parameter): #2 增加形参
time.sleep(2)
print("this is a print function")
return "run over"
print_function()
为了装饰带有参数的函数,需要给装饰器中的deco()和func()也加上参数,考虑到参数数量的不确定性和不同类型(普通参数和关键字参数),参数采用可变参数(**args)和可变关键字参数(**args)两者的组合,这样就可以适应有各种参数的函数。
import time
def timer(func):
def deco(*args,**kwargs):
time_start=time.time()
value=func(*args,**kwargs)
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return value
return deco
@timer
def print_function(parameter): #2 增加形参
time.sleep(2)
print("this is a print function",parameter)
return "run over"
print_function('with parameter') # 输出 this is a print function with parameter
5 带有参数的装饰器
有时候,同一个装饰器,针对不同的输入函数要采取不同的装饰方式。因此就需要有参数来标记对哪个函数采取哪种装饰措施,这种带有参数的装饰器的使用如下:
@timer(parameter=parmeter_value)
比如我们有两个print_function,分别是print_function1 和 print_function2,如下所示:
def print_function1():
time.sleep(1)
print("this is the print function 1")
def print_function2()
time_sleep(2)
print(this is the print function 2)
针对不同的打印函数,除了都需要计时外,装饰器还要针对传入的两个不同函数输出“in print_function1”和“in print_function1/print_function2”,这个时候装饰器就需要有参数来区分传入的函数为print_function1还是print_function2,不废话,上代码:
import time
def timer(parameter):
def outer(func): #1 增加一层函数
def deco():
print("in",parameter) #1 针对不同的输出不同的语句
time_start=time.time()
func()
time_end=time.time()
print("print function uses %f seconds"%(time_end-time_start))
return deco
return outer
@timer (parameter="print_function1")
def print_function1():
time.sleep(1)
print("this is the print function1")
@timer(parameter="print_function2")
def print_function2():
time.sleep(2)
print("this is the print function2")
print_function1() # 输出 in print_function1
print_function2() # 输出 in print_function2
由于要把paramter 参数传递到装饰器中,我们以往都是将函数名传入装饰器,现在多了一个参数,要怎么解决呢。一个自然的想法就是直接在原来timer(func)中直接增加一个参数,变为time(func, parameter),但是这样直接将两个参数放到一起会引起后面引用的错误。
实际的做法就是在 #1 处增加一层函数来接受参数,其产生的效果如下所示:
timer=timer(parameter)
print_function=timer(print_function)
其运行就和一般的装饰器一样了。
6、进一步阅读
其实装饰器是闭包思想的一个应用,关于闭包的介绍,请参考谈谈自己的理解:python中闭包,闭包的实质。
参考文章: