python 将计算结果保留到缓存中

Python057

python 将计算结果保留到缓存中,第1张

定义一个延迟属性的一种高效方法是通过使用一个描述器类,如下所示:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">class lazyproperty:

def init (self, func):

self.func = func

</pre>

你需要像下面这样在一个类中使用它:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">import math

class Circle:

def init (self, radius):

self.radius = radius

</pre>

下面在一个交互环境中演示它的使用:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">>>>c = Circle(4.0)

</pre>

仔细观察你会发现消息 Computing area 和 Computing perimeter 仅仅出现一次。

很多时候,构造一个延迟计算属性的主要目的是为了提升性能。 例如,你可以避免计算这些属性值,除非你真的需要它们。 这里演示的方案就是用来实现这样的效果的, 只不过它是通过以非常高效的方式使用描述器的一个精妙特性来达到这种效果的。

正如在其他小节(如8.9小节)所讲的那样,当一个描述器被放入一个类的定义时, 每次访问属性时它的 __get__() 、 __set__() 和 __delete__() 方法就会被触发。 不过,如果一个描述器仅仅只定义了一个 __get__() 方法的话,它比通常的具有更弱的绑定。 特别地,只有当被访问属性不在实例底层的字典中时 __get__() 方法才会被触发。

lazyproperty 类利用这一点,使用 __get__() 方法在实例中存储计算出来的值, 这个实例使用相同的名字作为它的property。 这样一来,结果值被存储在实例字典中并且以后就不需要再去计算这个property了。 你可以尝试更深入的例子来观察结果:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">>>>c = Circle(4.0)

</pre>

这种方案有一个小缺陷就是计算出的值被创建后是可以被修改的。例如:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">>>>c.area

Computing area

50.26548245743669

</pre>

如果你担心这个问题,那么可以使用一种稍微没那么高效的实现,就像下面这样:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">def lazyproperty(func):

name = ' lazy ' + func. name

@property

def lazy(self):

if hasattr(self, name):

return getattr(self, name)

else:

value = func(self)

setattr(self, name, value)

return value

return lazy

</pre>

如果你使用这个版本,就会发现现在修改操作已经不被允许了:

<pre style="box-sizing: border-boxfont-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", Courier, monospacefont-size: 12pxwhite-space: premargin: 0pxpadding: 12pxdisplay: blockoverflow: autoline-height: 1.4">>>>c = Circle(4.0)

我们经常谈论的缓存一词,更多的类似于将硬盘中的数据存放到内存中以至于提高读取速度,比如常说的redis,就经常用来做数据的缓存。 Python的缓存(lru_cache)是一种装饰在被执行的函数上,将其执行的结果缓存起来,当下次请求的时候,如果请求该函数的传参未变则直接返回缓存起来的结果而不再执行函数的一种缓存装饰器。

那它和redis的区别在哪?有什么优势?怎么使用? 下面为你讲解

1.现在我们先不使用缓存来写一个求两数之和的函数,并调用执行它两次:

执行结果

可以看到 test 被执行了两次,现在我们加上缓存再进行执行:

执行结果

可以看到 test 函数只被执行了一次,第二次的调用直接输出了结果,使用了缓存起来的值。

2.当我们使用递归求斐波拉契数列 (斐波那契数列指的是这样一个数列:0,1,1,2,3,5,8,它从第3项开始,每一项都等于前两项之和) 的时候,缓存对性能的提升就尤其明显了:

不使用缓存求第40项的斐波拉契数列

执行时间

使用缓存求第40项的斐波拉契数列:

执行时间

两个差距是非常明显的,因为不使用缓存时,相当于要重复执行了很多的函数,而使用了 lru_cache 则把之前执行的函数结果已经缓存了起来,就不需要再次执行了。

查看lru_cache源码会发现它可以传递两个参数: maxsize 、 typed :

代表被lru_cache装饰的方法最大可缓存的结果数量 (被装饰方法传参不同一样,则结果不一样;如果传参一样则为同一个结果) , 如果不指定传参则默认值为128,表示最多缓存128个返回结果,当达到了128个时,有新的结果要保存时,则会删除最旧的那个结果。如果maxsize传入为None则表示可以缓存无限个结果;

默认为false,代表不区分数据类型,如果设置为True,则会区分传参类型进行缓存,官方是这样描述的:

但在python3.9.8版本下进行测试,typed为false时,按照官方的测试方法测试得到的还是会被当成不同的结果处理,这个时候typed为false还是为true都会区别缓存,这与官方文档的描述存在差异:

执行结果

但如果是多参数的情况下,则会被当成一个结果:

执行结果

这个时候设置typed为true时,则会区别缓存:

执行结果

当传参个数大于1时,才符合官方的说法,不清楚是不是官方举例有误

当传递的参数是dict、list等的可变参数时,lru_cache是不支持的,会报错:

报错结果

缓存 缓存位置 是否支持可变参数 是否支持分布式 是否支持过期时间设置 支持的数据结构 需单独安装 redis 缓存在redis管理的内存中 是 是 是 支持5种数据结构 是 lru_cache 缓存在应用进程的内存中,应用被关闭则被清空 否 否 否 字典(参数为:key,结果为:value) 否

经过上面的分析,lru_cache 功能相对于redis来说要简单许多,但使用起来更加方便,适用于小型的单体应用。如果涉及的缓存的数据种类比较多并且想更好的管理缓存、或者需要缓存数据有过期时间(类似登录验证的token)等,使用redis是优于lru_cache的。