首先是装饰器是一种设计模式,具体的细节可以参考相关设计模式的书籍,
这里着重描述他在Python中的具体代码。
首先看一下装饰器的由来。
比如我有如下的代码:
def fun1():
sum_ = 0 #sum_后面跟下划线是为了跟 Python sum 函数不冲突。
for i in range(10000):
sum_ += i
return sum_
首先要有这样的概念,fun1 是一个提供了某个特定功能的函数,在这里就是
简单的把 0 - 9999 的数字累加。
通常情况下,我们只是简单的调用这个函数就可以使用这个功能了,比如
print(fun1())
可以得到函数执行的结果。
但是有些时候,我们希望知道这个函数的运行时间。那我们就得这样
import time
start_ = time.time()
result_ = fun1()
end_ = time.time()
print('result:', result_, ':time:', end_-start_)
这样能完成目的,但是有个问题,当我想计算另一个函数的运行时间的时候
那我就得修改一下代码了。
def fun2():
pass # 举例子
import time
start_ = time.time()
result_ = fun2()
end_ = time.time()
print('result:', result_, ':time:', end_-start_)
注意,当我们的需求变化时,如果我们必须靠修改 实现的内部细节的代码 的时候,
那这个代码的复用性是很差的。
需求变化代码固然得变化才能适应需求,但是变哪很重要。比如我要实现加法功能:
def add_(x, y):
return x + y
其实这个简单的函数也具有适应需求变化的能力,他的变化在 x, y 的值不同,
但是当 x, y 变化的时候,只需要把新的值当作参数传递过去就可以了。而不用修改
实现加法的内部代码。比如不好的代码实现 1 + 2
def add_():
return 1 + 2
当需求变成 3 + 4 就得修改源代码为:
def add_():
return 3 + 4
根据这个思路,我们再来分析一下刚才的代码,对于计算运行时间的功能来说,变化的
是要被测试的函数,那我们应该把该函数当作参数传递进来,也就是像这样:
def count_run_time(fun):
import time
start_ = time.time()
result_ = fun()
end_ = time.time()
print('time:', end_-start_)
return result_ #这里我们返回被测试函数的结果。
这个就是装饰器模式。这种写法能是不同功能的代码解耦,然后灵活的组合,完成代码复用。
如果是在静态语言中,装饰器的代码基本就完成了。但是Python不同 python是动态语言,可以把这个事情
做的更优雅。怎么优雅呢?
def count_run_time(fun):
def wrapper():
import time
start_ = time.time()
result_ = fun()
end_ = time.time()
print('time:', end_-start_)
return result_ #这里我们返回被测试函数的结果。
return wrapper # 注意这里的函数没有 () 也就是没有调用
fun1 = count_run_time(fun1)
fun1()
也就是既添加了新功能,还使得调用的时候跟以前一样,还是那个函数名。
比之前那个还要调用装饰器函数本身来说优雅多了。
再后来人们发现,这个装饰器的应用很多,虽然这样已经很优雅了,只需要
添加一行:
fun1 = count_run_time(fun1)
但是这一样完全是机械的行为所以人们又发明了 @ 语法,也被称为语法糖他是什么
作用呢?
@count_run_time
def fun1():
pass #...
我们可以直接要被装饰的函数的上面添加这样一行简介的代码。
然后就实现了
fun1 = count_run_time(fun1)
因为我们的装饰器会被用到任何一个函数上,每个函数的参数不同,为了适应这个需求我们的装饰器要修改一下
def count_run_time(fun):
def wrapper(*args, **kwargs): #这里改变了
import time
start_ = time.time()
result_ = fun(*args, **kwargs) #这里改变了
end_ = time.time()
print('time:', end_-start_)
return result_
return wrapper
使用可变参数完成这个功能。
这样貌似能理解这个语法的含义了,但是并没有,看下面的例子
如果我想让我的装饰器可以接受参数,就像:
def count_run_time(fun, n_bits=0):
def wrapper(*args, **kwargs): #这里改变了
import time
start_ = time.time()
result_ = fun(*args, **kwargs) #这里改变了
end_ = time.time()
if n_bits > 0:
format_str = 'time:%.'+str(n_bits)+'f'
else:
format_str = 'time:%f'
print(format_str%(end_-start_))
return result_
return wrapper
def fun1():
sum_ = 0
for i in range(10000):
sum_ += i
return sum_
装饰器函数可以指定显示时间时小数点后面的位数。
fun1 = count_run_time(fun1, 20)
print(fun1())
使用这种老式的语法这样做没什么问题。但是如果把他直接用到语法糖上呢?
@count_run_time(20)
def fun1():
sum_ = 0 # sum_ 后面跟下划线是为了跟 Python sum 函数不冲突。
for i in range(10000):
sum_ += i
return sum_
print(fun1())
Traceback (most recent call last):
File "/main.py", line 21, in <module>
@count_run_time(20)
File "/main.py", line 7, in wrapper
result_ = fun(*args, **kwargs) #这里改变了
TypeError: 'int' object is not callable
直接报错,怎么办。有人可能会说应该是这样把
@count_run_time(fun1, 20)
def fun1():
sum_ = 0 # sum_ 后面跟下划线是为了跟 Python sum 函数不冲突。
for i in range(10000):
sum_ += i
return sum_
你可以试试
那怎么办,
先在我们得先深究一下 @ 到底干了什么,实际是这样:
@count_run_time
def fun1():
pass
python 解释器会把 @ 后面的当作一个表达式,什么是表达式呢,就是可以
通过运算得出值的一系列操作的集合。他会运算这个表达式,然后需要表达式
返回一个可调用对象,也就是函数。我们的 count_run_time 本身就是个函数
。下一步他会调用这个函数然后把被装饰的函数的当作第一个也是唯一一个参数
传进去。
count_run_time(fun1)
然后这个调用也要返回一个可调用对象,把这个可调用对象赋值给 fun1
fun1 = count_run_time(fun1)
这样就完成了装饰器与被装饰函数的组装。
如果我们想传参的话,就要在解释器计算表达式的时候做文章。
我们不是直接给他一个可调用对象,而是这样:
def count_run_time(n_bits=0): #三层
def decorator(fun):
def wrapper(*args, **kwargs): #这里改变了
import time
start_ = time.time()
result_ = fun(*args, **kwargs) #这里改变了
end_ = time.time()
if n_bits > 0:
format_str = 'time:%.'+str(n_bits)+'f'
else:
format_str = 'time:%f'
print(format_str%(end_-start_))
return result_
return wrapper
return decorator #返回给解释器一个函数,该函数接受一个参数(被装饰的函数)。
@count_run_time(20) #这里是一个表达式,可以返回一个函数
def fun1():
sum_ = 0 # sum_ 后面跟下划线是为了跟 Python sum 函数不冲突。
for i in range(10000):
sum_ += i
return sum_
print(fun1())