Why
为什么要用装饰器?当我们写好了函数的时候,这个时候想增加一些与函数本身无关的功能,比如日志打印,运行时间等。如果直接修改原来的函数,函数比较多的时候会很麻烦;另外如果后期需要调整,那么针对每个函数都需要调整。
使用装饰器就可以解决这个问题,既能方便添加功能,又能简化代码和方便后期的修改,可以说是非常方便了。(平时发现重复代码比较多的时候,要想一下有没有办法简化代码。函数和装饰器都是简化代码的一种方式)
What
什么是装饰器?在了解装饰器之前,我们先了解下作用域和闭包。
作用域
def
命令行结果如下
hi
在这个过程中, inner 函数可以访问到外部嵌套函数的变量 name,即嵌套函数中内部函数能访问(仅读不能写)到外部函数的变量。
闭包
将上面的代码稍微修改一下
def
命令行结果如下
inner
这段代码与上面代码的主要不同在与:
- say_hi 函数没有调用 inner 函数,只是返回了 inner 函数
- 实际执行的时候,需要调用 a() 才能打印结果而不是 say_hi("michale")
另外可以发现,这里的 a 其实就是 sayhi 里面的 inner 函数。因为 Python 里面一切皆对象,所以 sayhi 函数这里可以返回一个函数对象,这个函数对象将 say_hi 函数里面的所有对象打包了一份。即使我们这个时候删除 say_hi 函数,a() 依然能打印出 name。
那么一个函数能解决的事情为什么要分成两个函数还要嵌套在这里添加复杂度呢?
- 避免全局变量的使用
- 提供某种形式的隐藏,内部函数的调用者无需知道外部函数的细节
闭包的几个要点
- 是嵌套函数,即在函数里面定义了另一个函数
- 外部函数返回了内部函数
- 内部函数引用了外部函数的变量
装饰器
在了解了闭包和作用域之后,我们把上面的代码再改一下
def
这里代码的改动主要是
- say_hi 函数现在接受的是一个函数对象而不是原来的字符串
- 接受的函数在 inner 函数内执行
这里的 a 实际上是指向的 inner 函数。say_hi 函数就是一个装饰器,同时也是闭包的一种情况,即闭包中内部函数引用的外部变量是一个函数对象。
上面这个写法看起来还是不太舒服,每次用的时候都需要重新生成一个函数然后再来调用它,比如这里的 a,不够优雅。幸好 Python 提供了 @ 语法糖,可以解决这个问题。比如上面的代码可以改为
def
这样看起来是不是好多了!人生苦短,我用 Python。
要注意的是我们这里虽然调用的时候是直接用的 hi(),但是这里的 hi 已经不是原来的 hi 了,而是 inner。hi = say_hi(hi)
How
看了上面的代码,感觉装饰器并没有什么实际用处啊?那看看下面这个
import
我们定义了一个函数 count_time来打印函数执行的时间,并用它来装饰 say_hi 函数。这样我们就不用在 say_hi 函数里面定义这些与函数本身无关的功能了,极大的降低了代码耦合。
可以看到上面的代码只是单纯的打印了 hi,那如果我想打印 hi, xiaowang, hi, xiaohong。要怎么办了。只需要对代码稍微修改即可
import
这里我们给 sayhi 函数和 inner 函数都定义了一个变量,这样在实际执行的时候,xiaowang 通过 inner 函数传递给了内层的函数 say_hi
可以看到我们上面打印的时间是到小数点后5位了,那对于执行时间比较长的函数来说,我们可能不需要关心这么多的位数,最好是能每次选择。因此我们的最终代码如下:
import
代码好像变得有点不同了,来我们慢慢思考
首先看一下 outter_count_time(position=7) 。这里好像跟之前不同了,但是其实还是一样的。这里的 outter_counttime(position=7) 返回的是 count_time 这个函数对象。
@outter_count_time
可以看成是
@count_time
另外为什么要在count_time 外面再定义一个函数呢?因为现有的 count_time 函数只能接受一个参数——函数名,为什么只能接受一个呢?因为 @ 语法糖的存在我们最后执行的 sayhi("xiaowang")实际上是 count_time(say_hi) ("xiaowang")。这种情况下 counttime 接受不了新的函数,而 say_hi 接收这个函数更不合适(因为打印时间这个功能与函数本身无关,这也是我们 为什么用装饰器的原因)。所以我们在外面加了一个函数,实际上的核心还是没变,你可以看作是 count_time 比原来的 count_time 多了一个 position 而已。
值得注意的是,如果我们在定义 outter_count_time 函数的时候使用了默认参数,那么不要以为我可以不传参数就可以写成下面这个样子
@outter_count_time
这里哪里错了呢?对,outter_counttime 后面掉了 ()。这个() 决定了say_hi 的装饰器是 outter_count_time 还是 outter_count_time 执行后返回的 count_time。很显然,我们想要的是 count_time,因此需要加上 () 让outter_counttime执行返回 count_time。