我也是一个Python初学者,空闲的时候学习一下Python,主要写一些营销用的脚本,或者简单的web页面。
最近又重温了一下装饰器,有了自己的一些理解,我来简单的解释一下什么是装饰器,只要你认真学过Python,肯定可以搞懂装饰器,它并没有那么神秘,只是玩了一手狸猫换太子的把戏而已。
正片开始:
现在,我们来定义一个函数,fight。这个函数需要3个参数,color,time,o,分别是颜色、时间、某个对象。
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
玩过moba游戏的都知道这是游戏开头的语音播报,我们就以这个举例。
现在我有一个需求,让你计算一下这个函数的运行时间,但是不能修改函数的内部代码。你会怎么做?
方案1
单凭你的直觉,不要想太多,你肯定立马就能想到,这也太简单了,只需要在函数的前后计算一下时间,然后算一下间隔就可以了。
start = time.time()
time.sleep(2)
fight('红色','5','小兵')
end = time.time()
print(end-start)
(由于案例的函数太简单,运行时间几乎是瞬间完成,所以用time模块睡了2秒)
这样就得到了函数的运行时间,很简单吧。
但是,实际工作中,需求肯定不是这么简单的,我们这个方法对单独一个函数这样操作没问题,实际工作中,我们可能要给大量的函数添加时间计算的功能,你总不能,每调用一次函数,就手动敲这些代码吧,作为程序员,不够优雅与方便,这种方案不可取。
方案2
既然有重复代码了,那我就用函数来解决重复的部分。经过一阵思考后:
在当前代码外面包裹一层函数不就可以了,同时把fight需要的参数,放在外层函数的形参里,就能传值了。
def wrapper(color,t,o):
start = time.time()
time.sleep(2)
fight(color,t,0)
end = time.time()
print(end-start)
wrapper("红色","5","小兵")
(这一步应该很好懂吧,只是函数的传参问题,不懂的话马上复习函数的基础知识,没必要往下看了。)
评价:这下我们把fight函数的参数写活了,让他用外部的wrapper函数的参数,这样就方便多了
但是问题,依旧存在,假如fight函数后续还需要其他参数呢?我们当初写这个函数的时候,只是初步传入了一些参数,后续的改动中,很可能需要加入新的参数。这是很常见的。
就像你的老板,永远会给你提新的需求。需求是无止境的。
方案3
这次你发狠了,行行行,要参数是吧,我就给你参数。
你想到了之前学习的可变位置参数、可变关键字参数,这里,你终于用上了
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
fight(*args,*kwargs)
end = time.time()
print(end-start)
wrapper('绿色','10','弓箭手')
(可变位置参数、可变关键字参数不懂的可以先复习下。
以方案3的代码为例,wrapper调用的时候,会先把传入的3个实参:绿色、10、弓箭手,打包为元祖(“绿色”,“10”,“弓箭手”)。
内层函数fight调用的时候,会把元祖给打散,依次拿到绿色、10、弓箭手这3个参数,这样就可以实现了随便传参的目的,你想咋传咋传)
评价:这一次,我们的方案终于很强了,可以随便传参,而且还不修改被装饰对象fight的代码。太强了!
但是有个问题,我们写的计时用的装饰器,只能为fight函数服务,其他函数享用不到,怎么让其他函数享用到呢。
就需要把fight函数给写活了。所以,我们还要继续优化我们的方案!
方案4:
怎么把函数wrapper写活呢,让它能为其他任何函数服务呢?
经过思考,我们发现
想把它写活,就需要把函数当做变量用。
func = fight
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
func(*args,*kwargs)
end = time.time()
print(end-start)
wrapper('绿色','10','弓箭手')
我们在wrapper外部定义一个变量 func,用它来接受fight的值,注意fight不要加括号,加了括号就等于调用函数了,我们这里只要fight的函数内存地址。
评价:这一次,我们使用一个变量func来接收被装饰函数的内存地址,大大增加了灵活性。问题依旧存在,
每次装饰某个函数,都需要先手动敲一下func=被装饰函数,然后再调用装饰函数,不够优雅,甚至是麻烦。
TIPS:
函数加了括号,等于调用了;单独的函数名,得到的是函数的内存地址。
方案5:
我们继续思考,之前为了给fight函数增加功能,我们在外部包了一层函数wrapper。这次我们还想给wrapper增加功能,那就如法炮制吧。
def outter():
func = fight
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
func(*args,*kwargs)
end = time.time()
print(end-start)
return wrapper # 把wrapper的内存地址返回去
res = outter()
res('紫色','3','龙王')
print(res) # res的值是wrapper的内存地址
这一次,我们在outter函数内部放了一个变量func来接收被被装饰函数fight的内存地址。
我们用res来接收outter()的返回值,outter()的返回值是wrapper的内存地址。
所以,我们使用 res(‘紫色’,‘3’,‘龙王’)调用函数,其实就是被装饰函数func在执行。
不过,还是有些小问题,因为我们把 func = fight放在outter函数内部了,这相当于把函数写死了,所以为了增加灵活性,我们需要把func放在outter的参数里。
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
func(*args,*kwargs)
end = time.time()
print(end-start)
return wrapper # 把wrapper的内存地址返回去
res = outter(func)
res('紫色','3','龙王')
print(res) # res的值是wrapper的内存地址
以后使用outter函数的时候,只需要给它传入一个函数的函数名(也就是函数的内存地址),不要带括号。
另外,为了让调用者不发现有异常,我们还可以做一些优化。
res = outter(func)
这行代码,我们可以改一下。,假如我们调用的是这次的案例函数,fight
我们可以改为
fight = outter(fight)
这样,使用者调用fight的时候,以为自己用的是最初的那个fight,实际上已经被我们来了一首狸猫换太子!
方案6
现在,还有一个问题,被我们忽略了 ,如果被装饰函数有返回值呢?
返回值是很重要的,丢失了返回值,是很严重的后果。
我们再来优化一下,很简单,用一个变量来接收被装饰函数的返回值,再把这个变量return出来就可以了。
def outter(func):
def wrapper(*args,**kwargs):
start = time.time()
time.sleep(2)
res = func(*args,*kwargs)
end = time.time()
print(end-start)
return res
return wrapper # 把wrapper的内存地址返回去
fight = outter(fight)
res('紫色','3','龙王')
print(res) # res的值是wrapper的内存地址
评价:这一次,我们再wrapper内部retrun了被装饰函数的返回值。在调用的时候,我们也有一个和被调用函数一样的变量来接收wrapper的内存地址。
对使用者来说,看起来和原功能一模一样。并且打印返回值,也和被装饰函数fight的返回值一模一样,做到了偷梁换柱!!!这也太酷了吧。
现在,还有一个小问题,我们每次使用outter的时候,都需要先进行赋值:fight = outter(fight),把被装饰函数fight的函数名放在参数里,总是写这一行代码,也不够优雅。
因此,Python为我们提供了一个简便的操作,就是语法糖。
我们只需要在被装饰函数的头部,写上 @outter 就可以了。
@outter
def fight(color,t,o):
print(f'我们出生在{color}方阵营')
print(f'敌军还有{t}秒到达战场')
print(f'{o}出击')
return "你真是个小天才"
这是不是你印象里的装饰器?到这里,是不是豁然开朗了?
以前很纳闷,为什么头顶加一个@,函数就变得这么强大了 ,现在你知道怎么来的吧。
这就是装饰器!在不修改被装饰函数的代码的情况下,给被装饰函数增加扩展功能,泰裤辣!
现在,我们再来做一些总结和提炼工作。
def outter(func):
def wrapper(*args,**kwargs):
# start = time.time()
# time.sleep(2)
res = func(*args,*kwargs)
# end = time.time()
# print(end-start)
return res
return wrapper # 把wrapper的内存地址返回去
先放上装饰器函数的代码,因为我们的刚才写的装饰器只是统计运行时间的。我们以后很可能会写其他功能的装饰器。
我们把统计时间的代码去掉,看下还剩什么。
def outter(func):
def wrapper(*args,**kwargs):
res = func(*args,*kwargs)
return res # 返回res,就是被装饰函数的返回值
return wrapper # 把wrapper的内存地址返回去
装饰器最核心的代码就被我们提取出来了,这个核心代码你一定要理解它是怎么来的,先自己尝试敲出来!。
熟练了之后,就可以把这段代码存为模板,以后再使用装饰器,直接调用模板即可,也无需手敲代码了。
另外,为了见名知意,我们最好把outter命名的更正式一些,包含具体功能的描述
比如刚才的统计运行时间,我们可以命名为 count_time()
以上代码运用的都是python的基础知识,基础可以的话,都是可以理解的。
谨记,理解不等于掌握,当时理解了,如果不练习,隔天就忘了。
强烈建议你自己手动敲出来,然后做笔记。如果再能分享,教会给其他人,你会掌握的更透彻。
本篇内容,也属于我自己的笔记总结。