• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Python函数式编程:装饰器

Python编程 彭东稳 7年前 (2017-09-21) 19861次浏览 已收录 0个评论

一、装饰器

Python 中的装饰器是通过利用了函数特性的闭包实现的,所以在讲装饰器之前,我们需要先了解函数特性,包括支持“嵌套函数及跨域访问”、“一个函数可以接收一个作为参数函数传入”、“函数可以返回一个函数”;另外也知道了函数也是一个对象,而且函数对象可以被赋值给变量。所以,通过变量也能调用该函数。以及闭包是怎么利用了函数特性的,这些相关函数特性构成了 Python 装饰器。所以,需要先看一下这篇文章:Python函数式编程:高阶函数与闭包

装饰器是一个很著名的设计模式,装饰器本质上是一个 Python 函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。装饰器经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

在面向对象(OOP)的设计模式中,decorator 被称为装饰模式。OOP 的装饰模式需要通过继承和组合来实现,而 Python 除了能支持 OOP 的 decorator 外,直接从语法层次支持 decorator。Python 的 decorator 可以用函数实现,也可以用类实现。

先看看一些实例,然后再来分析下原理。假设我们有如下的基本函数:

把函数赋值给一个变量:

函数对象有一个__name__属性,可以拿到函数的名字:

现在,假设我们要增强rge()函数的功能,比如,统计函数的执行时间,但又不希望修改 rge() 函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,decorator 就是一个接受函数作为参数,并且返回一个函数的高阶函数。所以,我们要定义一个能打印执行时间的 decorator,可以定义如下:

timeit() 函数的参数定义是 (*args, **kw)。因此,timeit() 函数可以接受任意参数的调用。

执行效果如下:

这种实现看上去还可以,但是每次调用的是 decorator,还要把函数作为一个参数传入。这样需要修改调用的地方,使用起来就不方便了。重新定义一下装饰器:

执行效果如下:

观察上面的 timeit,因为它是一个 decorator,所以接受一个函数作为参数,并返回了一个函数,然后赋值给一个变量。当我们真正需要执行时,调用这个变量即可。是不是发现 Python 装饰器就是结合了函数的相关特性而诞生的。

那么,在 Python 中使用装饰器有没有更简便的方法呢?自然是有的,Python 给我们提供了语法糖@,借助@语法,把装饰器装饰到相关的函数或类上面即可,这样使用起来就简便一些了。示例如下:

效果等效于上面的执行方法。

另外要说明的一点就是,一个装饰器在装饰一个函数或者类的时候就会进行实例化,然后返回函数。而函数只有在被装饰的函数或类做实例化时才会调用。如下测试,在要返回的函数前面输出一个字符:

然后同样去装饰一个函数,我们看一下被装饰的函数创建完成时,print("init")会不会输出:

可以看出创建完被装饰的函数之后,timeit 中定义的print("init")执行了,而wrap函数被返回了。当rge()函数被执行时才会执行wrap()函数。

另外,我们对一个函数或类定义多个装饰器以便实现不同的功能,那么被装饰时的执行顺序是从下往上执行。

二、带参数的装饰器

如果装饰器本身需要传入参数,那就需要编写一个返回装饰器的高阶函数。写出来会更复杂。比如,要自定义log的文本:

这个3层嵌套的 decorator 用法如下:

执行结果如下:

和两层嵌套的 decorator 相比,3层嵌套的效果是这样的:

我们来剖析上面的语句,首先执行 log(‘execute’),返回的是 decorator 函数,再调用返回的函数,参数是 now 函数,返回值最终是 wrapper 函数。其实这就是科里化过程,在函数式编程中非常有用的。

以上两种 decorator 的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的 ‘now’ 变成了 ‘wrapper’ :

因为返回的那个 wrapper() 函数名字就是 ‘wrapper’,所以,需要把原始函数的__name__等属性复制到 wrapper() 函数中,否则,有些依赖函数签名的代码执行就会出错。

简单来说,我们在使用 decorator 的过程中,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),如:__name____doc__等属性,一个是获取函数名,一个是获取函数内部注释信息的(”’docstring”’)。

为了消除副作用,如保持函数签名,不需要编写wrapper.__name__ = fun.__name__这样的代码,Python 的 functools 包中提供了一个叫 wraps 的 decorator 来消除这样的副作用。写一个 decorator 的时候,最好在实现之前加上functools的wraps,它能保留原有函数的名称和 docstring。functools 提供了两个 api,一个是 update_wrapper,一个是 wrap 装饰器函数,但是 wrap 装饰器函数也是调用了 update_wrapper。所以,一个完整的 decorator 的写法如下:

或者针对带参数的装饰器:

其中from functools import wraps导入解释器内置的 wraps 模块,模块的概念稍候讲解。现在,只需记住在定义 wrapper() 的前面加上@wraps()即可。这个装饰器就是帮我们保持函数签名的。当我们在装饰器中加上了@wraps()装饰器之后,再执行__name__时就恢复正常了。

三、实现wraps装饰器

上面说了,在 decorator 中使用 wraps 是用来帮我们消除副作用的。写一个 decorator 的时候,在实现之前加上 functools 的 wraps,它能保留原有函数的名称和 docstring,当还有一些其他信息。下面简单来看一下 wraps 实现方式:

首先我们定义一个简单的装饰器,如下:

然后装饰一下 run 函数,如下:

执行完成后,我们知道被装饰后的 run 函数产生了副作用。很多属性没有了。

好,现在我们知道大概问题了。我们试着不使用 wraps 来解决,第一个解决方法如下:

我们可以在装饰器内部,把被装饰函数原有的属性给改变回去。

如果每个装饰器都要这么写,重复步骤多了,肯定会烦。所以可以在这个基础之上把重复要做的哪一部分给抽出来变成一个可执行函数即可。如下代码:

现在,这个装饰器变成这样的了:

当我们再装饰 run 函数时,这个 run 函数对于我们已经定义的 name 和 doc 就不会有副作用了。

虽然说此时基本解决问题了,但是还是需要传入几个函数。我们可以在这个基础之上再次改造,变为一个可带参数的装饰器,也就是类似 functions.wraps 了。

然后改变一下我们log装饰器:

然后可以去装饰一下 log 函数试试,同样会消除副作用的。对于这个 wraps(fn)(wrap) 函数,就很像我们的装饰器了,传入一个函数,返回一个函数。所以在使用上就可以像 functions.wraps 一样使用了,如下:

再来使用 log 装饰一下我们的 run 函数,执行一下看看效果:

可以看到现在这种形式的使用方式就跟我们使用 functions.wraps 一样了。不同之处在于默认的 wraps 做的事情多于我们,可能更完善一些。

四、装饰器应用

1. 缓存

写一个函数装饰器,用来缓存函数的值。

Python 3 内置 functools 提供了一个 lru_cache 装饰器,就是用来提供缓存功能的,只不过 lru_cache 更加高级,支持 lru 算法,可设置内存最大缓存条目,当达到上限后就触发 lru 算法,把最近最少使用的 kv 删除。

如上我们设置最大缓存条目为 1,然后装饰 long_time_fun 函数,就是让这个函数睡眠。如果我们设置睡眠时间为 2 秒,那么第一次执行应该会等待 2 秒钟,同时 @lru_cache 会把 key 缓存到内存中,所以第二次执行就非常快了,不需要调用 long_time_fun 函数了。

第一次执行等待 2 秒,后面多次执行时间都会很快。另外,我们设置了最大缓存条目为 1,所以你可以再调用 long_time_fun 函数,给 3 秒睡眠,然后去验证看看前一个 2 秒的 key 是否失效了。

2. 监控

<延伸>

Python装饰器的诞生过程

你必须学写Python装饰器的五个理由

Python装饰器简介—这一篇也许就够了


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!