24. 装饰器语法与应用
Hi, 大家好。我是茶桁。
在最近几期的课程中,相信小伙伴们都频繁的看到一个词:「装饰器」, 那到底什么是装饰器,又有什么作用呢?我们这节课,就来好好的来了解一下。
装饰器定义
装饰器就是在不改变原有函数代码,且保持原函数调用方法不变的情况下,给原函数增加新的功能(或者给类增加属性和方法)。
**核心思想:**用一个函数(或者类)去装饰一个旧函数(或者类),造出一个新函数(或者新类)。
**应用场景:**引入日子,函数执行时间的统计,执行函数钱的准备工作,执行函数后的处理工作,权限校验,缓存等。
**语法规则:**在原有的函数上加上@
符,装饰器会把下面的函数当作参数传递到装饰器中,@
符又被称为「语法糖」。
装饰器原型
装饰器其实就是利用闭包,把函数当作参数传递,并在在函数内去调用传递进来的函数,并返回一个函数。
来,我们还是用代码来学习,先让我们定义一个普通函数:
现在让我们定义一个嵌套函数,分为外函数和内函数两部分:
这里面我们在内函数中打印了两句话,在两句话中间执行了一次外函数的参数(传递进来一个函数)。最后讲内函数作为参数返回。
然后我们讲刚才的普通函数old
作为参数传进去,然后再用外函数返回的inner
内函数重新赋值普通函数old
,最后让我们再执行一遍old
函数, 这个时候,因为old
被重新赋值,其实等同于调用了inner
函数。来,我们看看结果:
是不是稍显繁杂?那让我们换个思路,现在我们已经先定义好了outer
he inter
,两者关系不变。还是之前那些代码,那么我们如何利用装饰器来进行调用呢?
我们在定义old
函数的时候,直接加上一个@
语法糖,就将outer
作为了装饰器。这个装饰器的作用就等同于old = outer(old)
。那让我们打印看看结果:
那我们现在完成了装饰器的用法,按照定义,我们在不改变old
函数的代码,且保持了old
函数调用方法不变的情况下,增加了新的方法outer
。
old
函数经过outer
装饰器进行了装饰,代码和调用方法不变,但是函数的功能发生了改变。
你是不是这个时候又有疑问了,那装饰器要用在什么地方呢?让我们来实现一个应用:
装饰器应用:统计函数的执行时间
在正式写代码之前,我还是习惯带着大家先思考一遍。我们需要统计函数的执行时间,那我们需要什么关键点?
开始时间,
结束时间
开始时间和结束时间之间,就是程序在运行的过程。
好的,让我们来开始写代码,先来一段简单的需要运行的程序,为了能顺利统计时间,我们给它设定两个东西,一个循环,一个停止运行时长。这样,我们不会因为程序运行过快而看不到结果:
函数写完之后,让我们来执行一下看看:
没问题,确实是一秒打印一次。
现在,再来让我们完成要称为装饰器的统计函数:
在函数inner
中,我们最终是打印了最终的时间end
, 然后将整个inner
函数返回。
那么,让我们来尝试执行一下看看吧:
这样,我们就得到了func
这个函数最后执行的时间,那现在的问题是,统计时间的函数是一个通用函数,我们很多函数中都需要用到它进行统计。但是我们总不能所有的函数都要用这种方法重新赋值之后再调用吧?
那我们就用装饰器来解决就好了:
当然,最终这种函数的调用执行时间并不会像现在这样打印到前台,而是会写进log
变为日志存储起来,便于之后分析使用。
装饰器嵌套语法
在这一段代码中,我们来约个妹子,完成一场约会。从哪开始呢?就从找妹子要微信开始吧:
那这样,我们实现了一段最普通装饰器的定义。现在让我们在下面再定义一个装饰器函数,为什么呢?因为我渐渐不满足于只谈理想和人生了,要有点实际的行动了,顺便,我们写了一个列表,把和妹子要做的事情都列了个顺序,再来看看:
这个...顺序似乎不太对啊。让我们改变一下装饰器的顺序试试:
这回没错了,我们在最开始要妹子微信和最后送妹子回家中间,又进行了点什么。也算是有些进展了。那么,我们怎么去理解这个程序运行顺序呢?
先使用离得最近的
begin
装饰器,装饰love
函数,返回了一个begin_inner
函数在使用上面的
evolve
, 装饰了上一次返回的begin_inner
函数,又返回了一个evolve_inner
函数。
在调用完成之后,就是需要顺序执行了,其执行的嵌套关系和顺序如下:
那这,就是我们嵌套装饰器的用法。当然,这种嵌套装饰器的用法并不常见,可是一旦我们遇到了,要理解他的运行机制和顺序,避免不必要的麻烦。
装饰带有参数的函数
上一个部分,我们做了一个约会妹子的函数,并且使用装饰器进行了装饰。使的我们成功的按照进度依次执行了自己的计划。
但是问题来了,我们到目前为止约会了那么多妹子,都不知道谁是谁(海王体质),这可怎么办。这次,我们吸取教训,先要名字,既然之前的流程很成功,我们直接拿来使用就行了。但是繁杂的步骤我们都去掉,直奔主题:
一个简单的函数,并且是一个填空题,你愿意待谁去哪里畅谈人生,随便。比如我,找「露思」去,去了哪里,恕不奉告了:
可是即便如此,该有的流程还是不能丢,总不能凭空变出个妹子吧,还是得把必要的流程加上,原本定义的装饰器函数似乎不能使用了:
流程上现在是没问题了,可是我们执行一下发现,报错了。
进行不下去了吧?那没办法,谁叫你之前和之后都把人家名字忘了呢,海王也得有点职业道德才行。既然我们在执行的时候有参数,那你整个过程中都得带上才行。不能玩着玩着忘记人家名字,对吧。让我们改进一下,既然我们已经知道,在使用装饰器装饰过后的love()
执行实际上是执行装饰器inner
, 那我们尝试给inner
加上参数进行传递,还有很重要的,我们之前和之后,得把妹子名字记清楚才行,所以执行的时候也记得加上:
嗯,这样一场和「露思」妹子之间完美的从认识到约会流程就完成了。我们总结一下:
如果装饰器带有参数的函数,需要在内函数中定义形参,并传递给调用的函数。因为调用原函数等于调用内函数。
装饰多参数的函数
上一个流程跑完之后呢,我觉得还是不太妥当。主要是其中有两个选择题,一个是谁,一个是去哪里。对吧。再说了,我也得跟妹子自我介绍一下,加强一点印象。
好,我们这次再多设置几个参数,让整个约会过程更完善一些,那我们从最初就要规划一下,需要的参数包括:我,妹子,地点,行为
等等。还蛮多的。
这样我们就定义好了,至于*args
以及**kwargs
是什么,可以翻看之前的教程。
让我们现在来执行一下:
这样,多道选择题就被我们一一的化解了。相信「露思」妹子对我们的整体安排也是相当的满意了。
带有参数的装饰器
在我们平时使用 Python 各种第三方库的时候,不可避免的会遇到带有参数的装饰器。比如说,Django
框架中的@login_required(redirect_field_name="my_redirect_field")
。
这种带有参数的装饰器是干嘛的呢?还是拿我们之前的海王约会流程来举例。之前的流程是都没有什么问题,可是有没有发现,所有事情都是我们自己做主了,似乎妹子一直都没有反对过,也没有说自己想要什么。这是不是不太符合现实?
没错,我们也要给妹子装上一个会思考的大脑,也要学会做判断,好,让我们来实现一下:
既然我们这节是学习带有参数的装饰器,那么必然装饰器上是带参数的。呃,不要认为这句话是废话,我们看代码:
就像这样,我们给装饰器加上了参数。
那么现在问题就来了,我们来看看我们之前写的装饰器函数:
发现有什么问题了么?虽然我们的外层函数outer
是有形参的,但是我们之前的过程中了解到,这个形参f
是为了接收当前执行函数的。那还有什么其他地方接收非函数的普通参数嘛?
既然outer
中很重要的作用,除了接收函数在内函数内执行,还有一个就是返回inner
内函数,那么我们在不改变outer
的基础之上,再加一个接收普通参数的函数不就行了。
定义完成之后,我们来执行一下看看:
家人们谁懂啊,妹子真把我当海王了嘛?最后,我还是老老实实的接受了妹子的好意。
从整段代码中我们可以看出来,如果装饰器中有参数,需要有一个外壳函数来接收参数,传参之后就会进入到下一层函数中,并且传递当前对象。再然后才会再进入下一层中去。当然,我们在这里,利用传递的参数写了一段if
判断,用于确定妹子的决定是什么。然后我们就返回哪个决定的函数。最后别忘记,在外壳函数中,我们还需要讲outer
函数返回出去。此时虽然love
函数是outer
函数,但是在之前,put
装饰器已经将参数传递给了外壳函数put(var)
。在装饰器函数的争端代码中,我们都没有再执行过传进来的参数,也就是函数love()
, 所以此段代码中love
函数中的打印方法并未执行。
其执行步骤为:
用类装饰器装饰函数
之前我们所有的代码中,装饰器一直使用的都是函数装饰器。那我们能否用类来当装饰器装饰函数呢?
试试不就知道了。
写完了,这下我们省略了那么多杂七杂八的流程,因为我发现,那么多流程下来,最终感动的人只有自己。妹子愿意,怎么都愿意,不愿意的,无论做多少都不愿意。
来,让我们跑一下程序试试:
没问题,正确的执行。那这个时候的love
函数到底是什么呢?我们来打印出来看看:
可以看到,此时的love
函数就是Outer
类中的inner
函数。那我们怎么理解整个代码呢?
我们在love
函数上使用了装饰器Outer
, 那么这个时候Outer
就会实例化出来一个对象obj
,然后这个@obj
就等同于obj(love)
。
然后我们实例化对象进入Outer()
内部,进入之后遇到了魔术方法__call__
, 它会把该类的对象当作函数调用时自动触发。也就是obj()
触发。
还记得类的实例化么?会传入一个参数,也就是实例化对象本身:obj
。并且,第二个参数func
用来接收了传递进来的函数love
, 设置了self.func = func
, 把传进来的函数作为对象的成员方法。最后返回了一个函数inner
, 这个返回的函数是类中定义好的,于是作为实例化对象也将这个成员方法继承了下来,所以self.inner
可以直接被返回出去。
这个定义好的inner
接收了两个形参,一个是实例化对象本身,一个就是传递进来的函数love
的参数who
。然后,中间执行了一下魔术方法__call__
内定义好的self.func(who)
,实际上也就是obj.love(who)
。
看着迷糊?这样,我写一个注释过的完整版本。
不知道这段注释代码加上刚才的解说,大家能否看懂?有没有发现,用类做装饰器比起函数装饰器反而更清晰一点?不需要写那么多外层函数和内层函数。
让我们继续...
类方法装饰函数
刚才我们将整个类都用作了一个装饰器,那我们思考一下,是不是我们还可以用类中的方法来做装饰器呢?
说干就干,直接上代码测试:
在经历了几场约会之后,我们的耐心也渐渐没了。连是谁都不管,也没耐心去谈理想了。喝点茶聊聊天就直奔主题了都是。
到目前为止以上所有形式的装饰器,包括「函数装饰器」、「类装饰器」、「类方法装饰器」,都有一个共同特点:都是在给函数去进行装饰,增加功能。
那我们这个时候就不满足了,既然能装饰函数,那是否也能装饰类呢?
用装饰器装饰类
还真有一种装饰器是专门装饰类的,也就是在类的定义的前面使用@
装饰器这种语法。和装饰函数并无什么区别,只是放在了类前面而已:
装饰器给函数进行装饰,目的是不改变函数调用和代码的情况下给原函数增加新的功能。
装饰器给类进行装饰,目的是不改变类的定义和调用的情况下给类增加新的成员(属性或者方法)。
来,让我们具体的看看:
函数装饰器装饰类
这样,我们在原来的Demo
这个类中,使用装饰器增加了一个成员方法func2
,并且增加了一个成员属性name
, 并最终返回到原始类中。从而扩展了这个原始类Demo
中的方法和属性。
类装饰器装饰类
在之前那么多案例过后,相信大家这一段代码应该能看的出来吧。
那我这个地方要处一个思考题了,请问:此时的 obj
这个对象,是哪个类的对象。Demo
还是expand
?
这个问题的答案,我放在源码中了,大家要记得思考之后再去看答案。
那么,本节课的内容到这里也就结束了。
课程进行到这里,我们 Python 本身的所有内容就已经介绍完了。下节课开始,我们就要考试讲第三方库。
好,下课。
版权声明: 本文为 InfoQ 作者【茶桁】的原创文章。
原文链接:【http://xie.infoq.cn/article/5bfd6941fe136aca974f48e1c】。文章转载请联系作者。
评论