A. Python性能提升神器!lru_cache的介绍和讲解
我们经常谈论的缓存一词,更多的类似于将硬盘中的数据存放到内存中以至于提高读取速度,比如常说的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的。
B. python 缓存记忆化 @lru_cache()
使用孝手装饰器lru_cache加速函数计算
lru是一种缓存淘汰算法
(least recently used)即最近最少使用淘汰算法
不用lru_cache
使用lru_cache
lru_cache的定义如下,
第一个参数maxsize控制最大缓存数量,
第二个参数为True则严枯粗格检查被装饰函数的参数类没慎镇型
C. python3_原生 LRU 缓存
原生 LRU 缓存(最低 Python 版本为 3.2)
目前,几猜察乎所有层面上的软件和硬件中都需要缓存。Python 3 将 LRU(最近最少使用算法)缓存作为一个名为“lru_cache”的装饰器,使得对缓存的使用非常简单。
下面是一个简单的斐波那契函数,我们知道使用缓存将有助于该函数的计算,因为它会通过递归多次执行相同的工迟兆唯作。
现在,我们可以使用“lru_cache”来优化它(这种优化技术被称为“memoization”)。通过这种码培优化,我们将执行时间从几十秒降低到了几秒。
D. python有缓存模块吗
从Python 3.2开始,可以使用functools库中的装饰器@lru_cache。这是最近使用过的缓存,所以其中的项目没有到期时间,但作为快速入侵,它非常有用。
from functools import lru_cache
@lru_cache(maxsize=256)def f(x):
return x*xfor x in range(20):
print f(x)for x in range(20):
print f(x)
E. 设计一个LRU缓存淘汰算法
从结构上来看,双向链表可以支持O(1)时间复杂度的情况下找到前驱结点,正是这样的特点,使得双向链表在某些情况下的插入、删除操作比单链表更加高效。
我们已经找到要删除的结点,但是删除某个结点q,需要其前驱结点,而单链表不支持直接获得前驱结点,所以为了找到前驱结点,需要从头开始遍历链表,直到p<-next=q,说明p是q的前驱结点。对于双向链表来说,这丛键种情况就比较有优势哗烂。因为双向链表已经保存了前驱结点的指针,不需要像单链表那样遍历。单链表删除操作需要O(n)的时间复杂度,而双链表只需要O(1)的时间复杂度。
题目描述:
实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void set(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组“关键字-值”。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值渗芦巧,从而为新的数据值留出空间。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
F. python函数可以提高代码执行速度吗
不同的编程语言,会有不同 的缓存策略,例如,通过哈希映射、优先级队列等实现缓存。因此,不同的编程语言,在缓存的解决方案方面具有很大差异,可能需要几分钟,也可能需要几小时。
但是,在Python中,标准工具包functools实现了一种名为LRU(Least Recently Used)的缓存策略,可以通过传入参数,来设定缓存最近多少次的计算结果,如果传入参数为None,那么则不缓存。
G. FIFO和LRU小结
一:FIFO算法
1.0,FIFO (First in First out) 先进先出(核心原则:最先进来的,最先淘汰); 其实在操作系统的设计理念中很多地方都是利用到了先进先出的思想就是因为这个原则简单切符合人们的惯性思维,具备公平性实现起来也简单,直接使用数据结构中的队列即可实现
1.1,在FIFO 中应该支持这些操作: 一,get(key),如果Cache中存在该key,则返回对应的value值,否则返回 -1; 二,set(key,value),如果Cache中存在该key,则重置value值,如果不存在则将该key插入到Cache中,若Cache已满则淘汰最先进入Cache的数据
1.2,那么利用什么数据结构来实现呢?有这一种思路, 利用一个双向链表保存数据,当来了新数据之后便添加到链表末尾,如果Cache存满数据,则把链表头部数据删除,然后把次年数据添加到链表末尾,在访问数据的时候,如果在Cache中存在该数据的话,则返回对应的value值,否则返回 -1,如果想提高访问效率,可以利用hashmap来保存每个key在链表中的位置(参考下面拓展)
二:LRU算法
1.0,LRU (Least recently used) 最近最久未使用调度,其核心思想是"如果数据最近被访问过,那么将来被访问的几率也更高"; 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
1,新数据插入到链表头部
2,每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3,当链表满的时候,将链表尾部的数据丢弃
1.1,LRU的优缺点 1.命中率,当存在热点数据时,LRU的效率很好,但偶发性的,周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重 2,实现相对简单 3,命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部
2.0,LRU-K K代表最近使用的次数,因此LRU也可以认为是LRU-1,它主要是为了解决LRU算法"缓存污染"的问题,其核心思想是将"最近使用过1次"的判断标准扩展为"最近使用过K次"; 相比LRU要多维护一个队列,用于记录所有缓存数据被访问的历史,只有当数据的访问次数达到K次的时候,才将数据放入缓存.当需要淘汰数据时,LRU-k会淘汰第K次访问时间距离当前时间最大的数据.详细实现如下:
2.1,一,数据第一次被访问,加入到访问历史列表; 二,如果数据在访问历史列表里后达到K次访问,则按照一定(FIFO, LRU)淘汰; 三,当访问历史队列中的数据访问次数达到k次后,将数据l索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序; 四,缓存数据队列中被再次访问后,重新排序; 五,需要淘汰数据时,淘汰缓存队列中排在末尾的数据(即:淘汰倒数第K次访问离现在最久的数据)
2.2,LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史记录缓存或者清除掉
2.3优缺,LRU-K降低了"缓存污染"带来的问题,命中率比LRU要高,但LRU-K队列是一个优先级队列,算法复杂度和代价相对LRU较高,并且LRU需要记录那些被访问过,但是没有达到K次也就是还没有放入缓存的对象,因此b内存消耗会比LRU要多,当然如果数据量很大的时候,内存消耗会比较可观
3.0,Two queues (2Q) 算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(历史队列,还没有缓存数据)改为一个FIFO缓存队列,即: 2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列.
3.1,当数据第一次访问时,2Q算法会将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据; 一,新访问的数据插入到FIFO队列, 二,如果数据在FIFO队列中一直没有被再次访问,则最终按照FOFO规则淘汰, 三,如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部, 四,如果数据在LRU队列再次被访问,则将数据移到LRU队列头部, 五,LRU队列淘汰末尾的数据
3.2,可能会感觉FIFO队列比LRU队列短,但并不代表这是算法的要求,实际应用中两者比例没有硬性要求
3.3,2Q算法命中率高于LRU,切需要两个队列,但两个队列本身都比较简单,代价是FIFO和LRU代价之和; 2Q算法和LRU-2算法命中率类似,内存消耗也比较接近,但对于最后的缓存数据来说,2Q减少一次从原始储存读取数据或者计算数据的操作
4.0,Multi Queue (MQ) 算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据
4.1,MQ算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级,访问优先级是根据访问次数计算出来的,详情: 一,新插入的数据放入Q0; 二,每个队列按照LRU管理数据; 三,当数据访问次数达到一定次数需要提升优先级时将数据从当前队列删除,加入到高一级的队列头部; 四,为了防止高优先级数据永远不被淘汰,每个队列淘汰数据时,将数据从缓存中删除,将数据加入Q-history头部; 五,需要淘汰数据时,从最低一级队列开始按照LRU淘汰,每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部; 六,如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部; 七,Q-history按照LRU淘汰数据的索引
4.2,MQ降低了"缓存污染"带来的问题,命中率比LRU高,但MQ需要维护多个队列,切需要维护每个数据的访问时间,复杂度比较高,并且MQ需要记录每个数据的访问时间,需要定时扫码所有队列,代价也比较高
4.3,虽然MQ的队列看起来数量比较多,但由于所有队列之和受限于缓存容量的大小,因此这里多个队列长度之和和一个LRU队列是一样的,因此队列扫码性能接近
小结: 命中率 LRU-2 > MQ(2) > 2Q > LRU ; 复杂度 LRU-2 > MQ(2) > 2Q >LRU ; 代价 LRU-2 > MQ(2) > 2Q > LRU ; 需要注意的是,命中率并不是越高越好,实际中应该根据业务需求和对数据的访问情况进行分析选择,LRU虽然看起来命中率低一些,切存在"缓存污染"的问题,但其简单切代价小,实际中反而应用更多
拓展:基于 双链表的 LRU 实现: 一,传统意义的LRU算法是每一个Cache对象设置一个定时器,每次Cache命中则给定时器 +1,而Cache用完需要淘汰旧内容,放置新内容时就查看所有的计时器,并将使用的内容替换掉; 其弊端很明显,如果Cache的数量少,问题不大,但如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计时器,其性能与资源消耗巨大,效率也就非常的慢了; 二,双链表原理,将Cache的所有位置都用双链表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入Cache直接加到链表头中,这样在多次进行Cache操作后,最近被命中的就会被向链表头方向移动,而没有命中的则向链表后部移动,链表尾则表示最近最少命中的Cache,当需要替换内容时我们只需要淘汰链表最后的部分即可!
如果错误或者建议,欢迎下方留言,谢谢!
H. 你可能没有在Python3中使用但却应该使用的东西
由于Python EOL的发布,许多人开始将他们的Python版本从2切换到3。不幸的是,我发现大多数Python3看起来仍然像Python2,但是要加括号(尽管在我之前的文章《使用Python进行web抓取介绍》中的代码示例也是这样)。下面,我将展示一些令人兴奋的特性示例,你只能在Python3中使用它们,希望它可以让你使用Python解决问题变得更容易。
所有示例都是在Python 3.7中编写的,每个特性都包含该特性所需的最低Python版本。
在任何编程语言中,没有字符串是很难做任何事情的,为了保持理智,你希望有一种结构化的方法来处理字符串。大多数使用Python的人更喜欢使用format方法。
除了format,Python 3还提供了一种通过f-strings进行字符串插值的灵活方法。和上面一样使用f-strings的代码是这样的:
f-strings非常棒,但是有些字符串(比如文件路径)有自己的库,这使得它们的操作更加容易。Python 3提供了pathlib作为一个处理文件路径的方便抽象。如果你不确定为什么你应该使用pathlib,试着阅读这篇优秀的文章——《为什么你应该使用pathlib》——Trey Hunner。
静态和动态类型是软件工程中一个热门的话题,几乎每个人都对此有自己的看法。我将让读者决定何时应该编写类型,但我认为你至少应该知道Python 3支持类型提示。
Python 3提供了一种通过Enum类来编写枚举的简单方法。枚举是封装常量列表的一种方便的方法,因此它们不会在没有太多结构的情况下随机分布在你的代码中。
枚举是一组符号名称(成员),它们绑定到惟一的常量值。在枚举中,可以通过标识对成员进行比较,并且枚举本身也可以被遍历。https://docs.python.org/3/library/enum.html
缓存存在于我们今天使用的几乎所有水平的软件和硬件中。Python 3通过将LRU(最近敏模最少使用的)缓存公开为一个名为lru_cache的装饰器,使得使用它们变得非常简单。
下面是一个简单的Fibonacci函数,我们知道它将从缓存中受益,因为它通过递归多次执行相同的任务。
现在我们可以使用lru_cache对其进行优化(这种优化技术称为memoization(记忆化))。执行时间从几秒降到几纳秒。
请查看代码(文档 https://www.python.org/dev/peps/pep-3132/ )。
Python 3引入了数据悄迟类,这些数据类并没有太多限制,可以使用它们来减少样板代码,因为装饰器会自动生成特殊的方法,比如__init__() 和__repr()__。根据官方建议,它们被描述为“具有默认值的可变命名元组”。
使用数据类的Armor的相同实现。
结构化Python代码的一启拿李种方法是在包中(带有一个__init__.py文件的文件夹)。下面的示例是由官方Python文档提供的。
在Python2中,上面的每个文件夹都必须有一个__init__.py文件,它会将该文件夹转换为一个Python包。在Python3中,随着隐式命名空间包的引入,这些文件就不再需要了。
编者注:正如一些人所说,这并不像我在本节中指出的那样简单,从官方的PEP 420规范来看——__init__.py对于普通包仍然是必需的,将它从文件夹结构中删除将会把文件夹变成一个带有附加限制的本地命名空间包,关于本机命名空间包的官方文档对此展示了一个很好的例子,以及命名所有的限制。
就像互联网上几乎所有的列表一样,本列表并不完整。我希望这篇文章至少向你展示了一个你以前并不知道的Python 3功能,它将帮助你编写更简洁、更直观的代码。一如既往,所有的代码都可以在GitHub上找到。
(https://github.com/Weenkus/DataWhatNow-Codes/blob/master/things_you_are_probably_not_using_in_python_3_but_should/python%203%20examples.ipynb )