python -- 装饰器入门

用例: 统计函数执行需要的时间

假设我们执行的一段代码的运行时间比我们预想的时间要久,而这段代码块有多个函数调用组成,我们有理由相信至少是其中的一个函数调用导致整个代码块产生了瓶颈。我们如何去发现导致瓶颈产生的原因呢?其中一个方法就是统计函数执行需要花费的具体时间。

让我们以一段简单的代码举例。有一个函数func_a(),代码如下:

def func_a(stuff):
	a()
	b()
	c()

时间统计的一个方法就是在每次调用func_a的时候,加上时间统计的代码。比如:

start_a = datetime.datetime.now()
func_a(current_staff)
end_a = datetime.datetime.now()
print("Elapsed Time = {0}".format(end_a - start_a))

这样的确可以完成我们的目标。但是,如果我们有很多地方都调用func_a函数呢?难道每次调用都要修改一下func_a附近的代码么?能不能只是修改一处代码就可以呢?回答是肯定的:可以!
只要将时间统计的代码放到func_a函数内部就可以了。所以,我们修改代码如下:

def func_a(stuff):
	start_a = datetime.datetime.now()
	a()
	b()
	c()
	end_a = datetime.datetime.now()
	print("Elapsed Time = {0}".format(end_a - start_a))

这么改写的好处如下:
1.不需要每次调用func_a都修改一次代码。
2.我们将代码写到了一处。如果需要继续修改代码,只需在一个地方修改代码,而不需要每个地方都修改一次。

至此,我们的目的达到了。但是现实情况是我们可能需要统计多处不同代码的执行时间。有些需求出现第一次,就可能出现第二次、第三次。所以我们可能需要不停地编程类似的代码:

def func_a(stuff):
	start_a = datetime.datetime.now()
	a()
	b()
	c()
	end_a = datetime.datetime.now()
	print("Elapsed Time = {0}".format(end_a - start_a))

def func_b(stuff):
	start_b = datetime.datetime.now()
	e()
	f()
	g()
	end_b = datetime.datetime.now()
	print("Elapsed Time = {0}".format(end_b - start_b))
    
def func_c(stuff):
	start_c = datetime.datetime.now()
	h()
	m()
	n()
	end_c = datetime.datetime.now()
	print("Elapsed Time = {0}".format(end_c - start_c))

这样是不是很不友好?我们需要做的是找到某种方法,避免对func_a、func_b、func_c每次都做相同的修改。

python是门特别的语言,一旦一个函数被定义了,就可以被传递给其它函数、赋值给变量、甚至作为函数的返回值。
这些特性为装饰器的出现奠定了基础。来看下面的代码,看大家是否知道标签A,B,C,D处都会有什么行为:

def get_func():
    print("inside get_func")                 
    def returned_func():                    
        print ("inside returned_function")        
        return 1
    print ("outside returned_func")
    return returned_func
   
returned_func()     	# A                         
x = get_func()	        # B                         
x                       # C                        
x()                     # D 

以下是执行结果:

>>> def get_func():
    print("inside get_func")                 
    def returned_func():                    
        print ("inside returned_function")        
        return 1
    print ("outside returned_func")
    return returned_func

>>> returned_func()
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    returned_func()
NameError: name 'returned_func' is not defined
>>> x=get_func()
inside get_func
outside returned_func
>>> x
<function get_func.<locals>.returned_func at 0x00000000032DB8C8>
>>> x()
inside returned_function
1
>>> 

结果解释:
A
返回一个NameError,并显示returned_func不存在。有人可能会说,我们在上面不是已经定义了么?是的,上面的确是定义了,但是是在get_func内部定义的。

B
执行外层函数的定义,但是没有执行内层函数的定义returned_func。

C
返回函数get_func()的返回值,也是一个函数

D
既然x是函数,就可以被调用。调用x就是调用returned_func。

回到开头问题的本身!!!
回到开头的问题,该如何解决呢。一个建议就是创建一个函数time_this,这个函数将其他函数作为自己的参数,并在内部将该参数进行一定的封装。比如:

def time_this(original_func):
	def new_func(*args,**kwargs):
		start = datetime.datetime.now()
		x = original_func(*args,**kwargs)
		end = datetime.datetime.now()
		print("Elapsed Time = {0}'.format(end - start))
		return x
	return new_func

接下来,测试一下我们的时间统计功能:

def func_a(stuff):
	a()
	b()
	c()
func_a = time_this(func_a)

def func_b(stuff):
	e()
	f()
	g()
func_b = time_this(func_b)
    
def func_c(stuff):
	h()
	m()
	n()
func_c = time_this(func_c)

我们看func_a,当我们执行func_a = time_this(func_a)的时候,我们用time_this的返回值替换了func_a,也就是我们用添加了时间统计代码的函数替换了func_a。

 

装饰器
上面的过程完成了我们的需求,但是看起来很丑陋,也便于阅读。所以,python的作者给我们提供了装饰器。

@time_this
def func_a(stuff):
	a()
	b()
	c()

上面个的定义就等价于:

def func_a(stuff):
	a()
	b()
	c()
func_a = time_this(func_a)

上面就是装饰器的语法糖。@没有什么神奇之处,就是约定的规则而已。

 

结论!
装饰器就是返回函数的函数。

 

 

如果你继续钻研,还可以看到装饰器的类:

@add_class_funcionality
class MyClass:
	...

 

带有参数的装饰器

@require_permission(name="edit"):
def save_changes(stuff):
	...

  

 

点赞