老样子,先来三个问题:
1. 什么是装饰器
2. 为什么要有装饰器
3. 怎么用装饰器
一、什么是装饰器
装饰器从字面意思理解来说就是具有装饰功能的器具。在python中,装饰器就是用来给被装饰对象(函数)增加功能(装饰)的工具。
装饰器本身可以是任意可调用的对象——函数
被装饰对象也可以是任意可调用的对象——函数
装饰器:写一个函数给另一个函数增加新功能
二、为什么要有装饰器
当我们写的程序正式进入运营阶段,我们需要遵守开放封闭原则。
开放封闭原则:程序上线以后就应该对修改封闭,对扩展开放
修改封闭:不能修改功能性函数的源代码
不能修改功能性函数的调用方式
扩展开放:可以为原有的功能添加新功能
这两个原则看上去挺矛盾的,其实仔细想一想是可以想通的。我们的程序上线以后,有客户在正常使用,当我们更改了函数源代码,可能会导致更改不小心出错,使程序崩溃。调用方式更改显得麻烦,要更改每次调用函数的地方,也容易出错。而我们的程序在刚出生时并不是十全十美的,我们必须要为其添加一些功能。
这个时候装饰器就派上了用场。
三、怎么用装饰器
我们一步步的将装饰器讲解清楚。首先,我们定义一个函数
import time, random
def msg():
print('I have something to tell you, please wait a moment.')
time.sleep(random.randint(1,5))
print('I like you.')
msg()
# 该函数会随机休眠1-5秒钟。
现在,我们想要给该函数增加一个计时的功能,看看函数具体运行了多久。
import time, random
def msg():
print('I have something to tell you, please wait a moment.')
time.sleep(random.randint(1,5))
print('I like you.')
start_time = time.time()
msg()
stop_time = time.time()
print('Run time is %s' % (stop_time - start_time))
这样子我们就实现了更能了,但是这样很麻烦,并且如果要重复使用计时功能,就要写重复代码,所以我们想到了将计时功能封装成一个函数
def wrapper():
start=time.time()
msg()
stop=time.time()
print('Run time is %s' % (stop-start))
wrapper()
这样又出现了新问题:我们更改了函数的调用方式,并且我们将wrapper函数写死了,只能对内部调用的msg函数进行计时,不符合我们想要的结果。这时,就需要我们将前面的知识点全部串起来:函数对象,函数嵌套,闭包函数,名称空间与作用域。
def timmer(func): # 这里运用的就是昨天学的闭包函数知识点
def wrapper(): # 定义wrapper函数
start=time.time()
func()
stop=time.time()
print('Run time is %s' % (stop-start))
return wrapper # 将wrapper函数的内存地址作为返回值返回。
a = timmer(msg) # 这里的a指向wrapper函数的内存地址
a() # a()就可以触发执行wrapper函数的代码块
我们将msg函数的内存地址作为参数传入timmer函数,这样我们就可以在wrapper函数中访问到msg这个参数。(闭包函数作用域知识点)。然后我们将wrapper函数的内存地址作为返回值返回,这样我们执行timmer函数就可以得到一个函数的内存地址。
def timmer(func): # 这里运用的就是昨天学的闭包函数知识点
def wrapper(): # 定义wrapper函数
start=time.time()
func()
stop=time.time()
print('Run time is %s' % (stop-start))
return wrapper # 将wrapper函数的内存地址作为返回值返回。
msg = timmer(msg) # 作为参数的msg是我们定义的msg函数的内存地址
# 作为变量名的msg是指向wrapper函数的内存地址
msg() # 此时我们这样写看似在调用msg函数,但实际上是调用了wrapper函数
# 这样子我们就实现了偷梁换柱,在外观上没有更改msg函数的调用方式。
看上面一段代码,我们把上面说的两个问题全部解决了。外观上没有更改msg的调用方式,并且实现了装饰器的多用性。可以将任意的函数名传入,实现给任意函数增加计时的功能。
可以看出,当传入的函数没有参数,没有返回值时,我们写的装饰器还是很完美的。但是当传入的函数有参数或者有返回值时,就先得略有不足。我们接下来再来修改一下:
def timmer(func):
def wrapper(*args, **kwargs):
strat_time = time.time()
res = func(*args, **kwargs)
stop_time = time.time()
return res
return wrapper
我们对wrapper函数写了两个可变长参数,这两个参数可以接收一切传进来的实参并原封不动的将其传递给func函数。当我们传入的函数需要参数时,*args与**kwargs可以全部接收,不需要参数时,可以不传入参数,完美实现了传入函数的参数需求。然后我们在wrapper函数内将func的运行结果赋值给res,再将res当做返回值返回,想一下就可以明白,wrapper函数的返回值和func函数的返回值是相同的。
这样,一个八分完美的装饰器就写好了。为什么说是八分完美呢,因为还有一个知识点咱们还没有提到。
装饰器也是可以有参数的。
在说含参装饰器之前,我们先来说一下装饰器的使用方法:语法糖
定义好装饰器以后,我们只需要在被装饰函数的正上方来一个@+装饰器名:
import time, random
def timmer(func):
def wrapper(*args, **kwargs):
strat_time = time.time()
res = func(*args, **kwargs)
stop_time = time.time()
return res
return wrapper
@timmer
def msg():
print('I have something to tell you, please wait a moment.')
time.sleep(random.randint(1,5))
print('I like you.')
msg()
@+装饰器名字就是装饰器的正确用法:@timmer会帮我们实现:正下方的函数名作为参数传入装饰器。再将函数名作为变量名去接收装饰器的返回值。
接下来我们说一下含参装饰器:
想一下,我们在什么情况下会用到给装饰器传入参数。在我们的装饰器内部需要使用一个参数的时候才会传入一个参数。而我们看装饰器的代码:
def timmer(func):
def wrapper(*args, **kwargs):
strat_time = time.time()
res = func(*args, **kwargs)
stop_time = time.time()
return res
return wrapper
可以看到,不管是wrapper还是timmer,都无法再传入参数,他们两个的参数已经固定死了,再传入其他参数的话就会出错。所以我们想到了用闭包函数给装饰器传参数。
说白了含参装饰器就是一个三层的闭包函数
def timmer_2(x):
def timmer(func):
def wrapper(*args, **kwargs):
strat_time = time.time()
print(x)
res = func(*args, **kwargs)
stop_time = time.time()
return res
return wrapper
return timmer
通过外包一层timmer_2,我们将变量x作为参数传进了装饰器内部。不管内部需要什么参数,我们都可以通过最外层将参数传进去。这样就实现了装饰器对参数的需求。
我们就学会了一个十全十美的含参装饰器。