什么是装饰器
先上概念:
装饰器(decorator)的功能是将被装饰的函数当做参数传递给装饰器对应的函数(名称相同),并返回包装后的被装饰的函数。
假如我们需要执行一个函数eat(),他的内容很简单,就是打印“おいしい”,这是日语“真好吃”的意思。
然而我们还想在打印这句话之前,先打印:“いただきます”(日语:我开动了),在执行eat()函数之后,打印“ごちそうさまでした”(日语:我吃好了)。这其实是一个很常见的打印日志的需求,(一般打印日志需要在调用函数前记录开始调用,在调用函数后记录结束调用)要想实现这个需求,应该如何写代码呢?
我们当然可以用最笨的办法,写成这样:
>>> def eat():
print('いただきます')
print('おいしい')
print('ごちそうさまでした')
>>> eat()
いただきます
おいしい
ごちそうさまでした
那么这样可不可以呢?不是不行,但这样操作最大的问题在于它把打印日志这个非核心业务逻辑放到了核心业务代码里面去实现,会让整个过程显得异常臃肿。降低程序执行速度。并且,这也不符合一个函数只做一件事情的原则。最大的坏处就是当我们需要修改一些内容的时候,我们需要dip in the sea of code,去一个个手动修改。
这个时候,装饰器就出场了。他的整体思路就是,打印日志只是一个装饰,而执行eat()函数才是核心业务。既然如此,我就把两者分开,这样一来,同一个装饰器,可以装饰eat()函数,也可以装饰drink()函数,还可以装饰成千上万的其他函数。岂不美哉?
嗯……听起来有点像我们之前学过的闭包对不对?我们来尝试一下coding。。。
>>> def decorator(fun):
def wrapper():
print('いただきます')
fun()
print('ごちそうさまでした')
return wrapper
>>> def eat():
print('おいしい')
>>> a = decorator(eat)
>>> a()
いただきます
おいしい
ごちそうさまでした
OK,我们成功地把上述的功能通过装饰器给实现了。在上面的代码中,eat()是主函数。decorator(wrapper())这个闭包是装饰器。我们来分析一下他的运行机制:
- 我们输入了a = decorator(eat),也就是把decorator(eat)赋值给了a
- decorator(eat)就是把外层函数decorator(fun)里面的参数fun给替换成了eat
- 因此,内层函数wrapper()先执行了以下内容:
3.1 print('いただきます')
3.2 eat()
3.3 print('ごちそうさまでした') - 执行完以上内容以后,它把内容都存在了return的这个decorator里,等着被唤醒。
- 我们输入了a(),相当于输入了decorator(),这个时候带着内容的,沉睡的decorator就被唤醒了,它根据命令,显示了自己的内容:
a). いただきます——decorator自己的内容
b). おいしい——引用的eat()函数的执行结果
c). ごちそうさまでした——decorator自己的内容
至此,整个装饰器已经全部完成了他的工作。当然,根据前面我自己发现的小窍门,我们分两步输入a = decorator(eat); a()这个过程,也可以简化成一步:
decorator(eat)()
>>> def decorator(fun):
def wrapper():
print('いただきます')
fun()
print('ごちそうさまでした')
return wrapper
>>> def eat():
print('おいしい')
>>> decorator(eat)()
いただきます
おいしい
ごちそうさまでした
>>>
Great ! 结果完全一样。
以上就是装饰器的使用场景,而python是一门优雅的语言。能一步完成的工作,绝不分成两步。因此python提供了一个更简单的使用装饰器的方式——@
def log(fun):
def wrapper():
print('start to call eat()...')
fun()
print('over to call eat()...')
return wrapper
@log
def eat():
print('Begin to eat!')
eat()
在上面的例子中,我们使用@log来表示调用装饰器的意思。可以理解为在一个微信群里,eat()函数@log(fun)函数,“大兄dei,该干活了!”
主函数带参数的情况
敏锐的小伙伴可能会发现,主函数eat()并没有使用参数啊,如果要使用参数了,会发生什么变化吗?是不是直接给eat()添加一个参数就可以了呢?
显然不是,如果eat(name)这里是有参数的,那么fun()里面肯定也必须保持一致,也必须要有参数,否则虽然给eat(name)传了一个参数Tom,但当eat作为log的参数替换掉fun的时候,执行的仍然是eat(),而不是eat(Tom).
OK,现在我们知道了,假如eat()函数改成了eat(name),fun()也要同步改成fun()那么,那么wrapper()要不要也改成wrapper(name)呢?
自然也是需要的,因为如果wrapper()这里没有name参数,那么fun()里面的name就不会成为一个参数,而是会成为一个“死”的字符串。那么当eat替换fun之后,eat就兴冲冲地带着参数Tom去找wrapper了,而wrapper()很诧异,你没告诉我你要带参数啊?你带个参数让我直接懵逼了呀。于是程序就报错了。
wrapper() takes 0 positional arguments but 1 was given
好了,我们举个例子吧:
>>> def decorator(fun):
def wrapper(name):
print('いただきます')
fun(name)
print('ごちそうさまでした')
return wrapper
>>> def eat(name):
print('%s says, おいしい' % (name))
>>> decorator(eat)('Tomy')
いただきます
Tomy says, おいしい
ごちそうさまでした
太棒了,现在我们有一个新的需求,我们想让“GLORIA,EVELYN,JEAN,CHERYL,MILDRED,KATHERINE,JOAN,ASHLEY,JUDITH,ROSE,JANICE,KELLY,NICOLE,JUDY,CHRISTINA,KATHY,THERESA”这些人全都say おいしい,有办法吗?
这里就要用到我们之前提到的收集参数了。只需要把name参数都改成收集参数即可。如果是我的话,一般我们将所有的参数都尽量使用收集参数,因为这样我就可以不用老去修改变量名了。
def log(fun):
def wrapper(*name):
print('いただきます')
fun(*name)
print('ごちそうさまでした')
return wrapper
@log
def eat(*name):
print(*name, 'said おいしい!')
eat('GLORIA','EVELYN','JEAN','CHERYL','MILDRED','KATHERINE','JOAN','ASHLEY','JUDITH','ROSE','JANICE','KELLY','NICOLE','JUDY','CHRISTINA','KATHY','THERESA')
いただきます
GLORIA EVELYN JEAN CHERYL MILDRED KATHERINE JOAN ASHLEY JUDITH ROSE JANICE KELLY NICOLE JUDY CHRISTINA KATHY THERESA said おいしい!
ごちそうさまでした