Python中的@cache有什么妙用?

news2025/1/23 8:03:17

Python中的@cache有什么妙用?

本文同步投给#创作纪念日#活动,2019年4月8日我在C站发了第一篇博文,至今200多篇了,感兴趣可以访问我的主页:小斌哥ge。
看到官方发的私信,是鼓励博主写一些感悟,由于最近比较忙,就不细聊内心活动了。将本来打算要发布的这篇文章顺便投到活动中。
感谢C站提供的平台,祝C站越来越好。

缓存是一种空间换时间的策略,缓存的设置可以提高计算机系统的性能。具体到代码中,缓存的作用就是提高代码的运行速度,但会占用额外的内存空间。

在Python的内置模块 functools 中,提供了高阶函数 cache() 用于实现缓存,用装饰器的方式使用: @cache。

@cache缓存功能介绍

在cache的源码中,对cache的描述是:Simple lightweight unbounded cache. Sometimes called “memoize”. 翻译成中文:简单的轻量级无限制缓存。有时也被称为“记忆化”。

def cache(user_function, /):
    'Simple lightweight unbounded cache.  Sometimes called "memoize".'
    return lru_cache(maxsize=None)(user_function)

cache() 的代码只有一行,调用了 lru_cache() 函数,传入一个参数 maxsize=None。lru_cache() 也是 functools 模块中的函数,查看 lru_cache() 的源码,maxsize 的默认值是128,表示最大缓存128个数据,如果数据超过了128个,则按 LRU(最久未使用)算法删除多的数据。cache()将maxsize设置成None,则 LRU 特性被禁用且缓存数量可以无限增长,所以称为“unbounded cache”(无限制缓存)。

lru_cache() 使用了 LRU(Least Recently Used)最久未使用算法,这也是函数名中有 lru 三个字母的原因。最久未使用算法的机制是,假设一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小, LRU算法选择将最近最少使用的数据淘汰,保留那些经常被使用的数据。

cache() 是在Python3.9版本新增的,lru_cache() 是在Python3.2版本新增的, cache() 在 lru_cache() 的基础上取消了缓存数量的限制,其实跟技术进步、硬件性能的大幅提升有关,cache() 和 lru_cache() 只是同一个功能的不同版本。

lru_cache() 本质上是一个为函数提供缓存功能的装饰器,缓存 maxsize 组传入参数,在下次以相同参数调用函数时直接返回上一次的结果,用以节约高开销或高I/O函数的调用时间。

@cache的应用场景

缓存的应用场景很广泛,如静态 Web 内容的缓存,可以直接在用户访问静态网页的函数上加 @cache 装饰器。

一些递归的代码中,存在反复传入同一个参数执行函数代码的情况,使用缓存可以避免重复计算,降低代码的时间复杂度。

接下来,我用斐波那契数列作为例子来说明 @cache 的作用,如果前面的内容你看完了还一知半解,相信看完例子你会茅塞顿开。

斐波那契数列是指这样一个数列:1、1、2、3、5、8、13、21、34、… ,从第三个数开始,每个数都是前两个数之和。斐波那契数列的代码实现不难,大部分程序员入门时都做过,在Python中,实现的代码非常简洁。如下:

def feibo(n):
    # 第0个数和第1个数为1
    a, b = 1, 1
    for _ in range(n):
        # 将b赋值给a,将a+b赋值给b,循环n次
        a, b = b, a+b
    return a

当然,斐波那契数列的代码实现方式有很多种(至少五六种),本文为了说明 @cache 的应用场景,用递归的方式来写斐波那契数列的代码。如下:

def feibo_recur(n):
    if n < 0:
        return "n小于0无意义"
    # n为0或1时返回1(前两个数为1)
    if n == 0 or n == 1:
        return 1
    # 根据斐波那契数列的定义,其他情况递归返回前两个数之和
    return feibo_recur(n-1) + feibo_recur(n-2)

递归代码执行时会一直递归到feibo_recur(1)和feibo_recur(0),如下图所示(以求第6个数为例)。

在这里插入图片描述

求F(5)时要先求F(4)和F(3),求F(4)时要先求F(3)和F(2),… 以此类推,递归的过程与二叉树深度优先遍历的过程类似。已知高度为 k 的二叉树最多可以有 2k-1 个节点,根据上面递归调用的图示,二叉树的高度是 n,节点最多为 2n-1, 也就是递归调用函数的次数最多为 2n-1 次,所以递归的时间复杂度为 O(2^n) 。

时间复杂度为O(2^n)时,执行时间随 n 的增大变化非常夸张,下面实际测试一下。

import time


for i in [10, 20, 30, 40]:
    start = time.time()
    print(f'第{i}个斐波那契数:', feibo_recur(i))
    end = time.time()
    print(f'n={i} Cost Time: ', end - start)

Output:

第10个斐波那契数: 89
n=10 Cost Time:  0.0
第20个斐波那契数: 10946
n=20 Cost Time:  0.0015988349914550781
第30个斐波那契数: 1346269
n=30 Cost Time:  0.17051291465759277
第40个斐波那契数: 165580141
n=40 Cost Time:  20.90010976791382

从运行时间可以看出,在 n 很小时,运行很快,随着 n 的增大,运行时间极速上升,尤其 n 逐步增加到30和40时,运行时间变化得特别明显。为了更清晰地看出时间变化规律,再进一步进行测试。

for i in [41, 42, 43]:
    start = time.time()
    print(f'第{i}个斐波那契数:', feibo_recur(i))
    end = time.time()
    print(f'n={i} Cost Time: ', end - start)

Output:

第41个斐波那契数: 267914296
n=41 Cost Time:  33.77224683761597
第42个斐波那契数: 433494437
n=42 Cost Time:  55.86398696899414
第43个斐波那契数: 701408733
n=43 Cost Time:  92.55108690261841

从上面的变化可以看到,时间是指数级增长的(大约按1.65的指数增长),这跟时间复杂度为 O(2^n) 相符。按照这个时间复杂度,假如要计算第50个斐波那契数列,差不多要等一个小时,非常不合理,也说明递归的实现方式运算量过大,存在明显的不足。如何解决这种不足,降低运算量呢?接下来看如何进行优化。

根据前面的分析,递归代码运算量大,是因为递归执行时会不断的计算 feibo_recur(n-1) 和 feibo_recur(n-2),如示例图中,要得到 feibo_recur(5) ,feibo_recur(1) 调用了5次。随着 n 的增大,调用次数呈指数增加,造成了海量不必要的重复,浪费了大量时间。

在这里插入图片描述

假如有一个地方将每个 n 的执行结果记录下来,当作“备忘录”,下次函数再接收到这个相同的参数时,直接从备忘录中获取结果,而不用去执行递归的过程,就可以避免这些重复调用。在 Python 中,可以创建一个字典或列表来当作“备忘录”使用。

temp = {}  # 创建一个空字典,用来记录第i个斐波那契数列的值


def feibo_recur_temp(n):
    if n < 0:
        return "n小于0无意义"
    # n为0或1时返回1(前两个数为1)
    if n == 0 or n == 1:
        return 1
    if n in temp:  # 如果temp字典中有n,则直接返回值,不调用递归代码
        return temp[n]
    else:
        # 如果字典中还没有第n个斐波那契数,则递归计算并保存到字典中
        temp[n] = feibo_recur_temp(n-1) + feibo_recur_temp(n-2)
        return temp[n]

上面的代码中,创建了一个空字典用于存放每个 n 的执行结果。每次调用函数,都先查看字典中是否有记录,如果有记录就直接返回,没有记录就递归执行并将结果记录到字典中,再从字典中返回结果。这里的递归其实都只执行了一次计算,并没有真正的递归,如第一次传入 n 等于 5,执行 feibo_recur_temp(5),会递归执行 n 等于 4, 3, 2, 1, 0 的情况,每个 n 计算过一次后 temp 中都有了记录,后面都是直接到 temp 中取数相加。每个 n 都是从temp中取 n-1 和 n-2 的值来相加,执行一次计算,所以时间复杂度是 O(n) 。

下面看一下代码的运行时间。

for i in [10, 20, 30, 40, 41, 42, 43]:
    start = time.time()
    print(f'第{i}个斐波那契数:', feibo_recur_temp(i))
    end = time.time()
    print(f'n={i} Cost Time: ', end - start)
print(temp)

Output:

第10个斐波那契数: 89
n=10 Cost Time:  0.0
第20个斐波那契数: 10946
n=20 Cost Time:  0.0
第30个斐波那契数: 1346269
n=30 Cost Time:  0.0
第40个斐波那契数: 165580141
n=40 Cost Time:  0.0
第41个斐波那契数: 267914296
n=41 Cost Time:  0.0
第42个斐波那契数: 433494437
n=42 Cost Time:  0.0
第43个斐波那契数: 701408733
n=43 Cost Time:  0.0
{2: 2, 3: 3, 4: 5, 5: 8, 6: 13, 7: 21, 8: 34, 9: 55, 10: 89, 11: 144, 12: 233, 13: 377, 14: 610, 15: 987, 16: 1597, 17: 2584, 18: 4181, 19: 6765, 20: 10946, 21: 17711, 22: 28657, 23: 46368, 24: 75025, 25: 121393, 26: 196418, 27: 317811, 28: 514229, 29: 832040, 30: 1346269, 31: 2178309, 32: 3524578, 33: 5702887, 34: 9227465, 35: 14930352, 36: 24157817, 37: 39088169, 38: 63245986, 39: 102334155, 40: 165580141, 41: 267914296, 42: 433494437, 43: 701408733}

可以看到,代码运行时间全都降到小数点后很多位了(时间太小,只显示了 0.0 )。不过,temp 字典里记录了每个数对应的斐波那契数,这需要占用额外的内存空间,用空间换时间。

上面的代码也可以用列表来当“备忘录”,代码如下。

temp = [1, 1]


def feibo_recur_temp(n):
    if n < 0:
        return "n小于0无意义"
    if n == 0 or n == 1:
        return 1
    if n < len(temp):
        return temp[n]
    else:
        # 第一次执行时,将结果保存到列表中,后续直接从列表中取
        temp.append(feibo_recur_temp(n-1) + feibo_recur_temp(n-2))
        return temp[n]

现在,已经剖析了递归代码重复执行带来的时间复杂度问题,也给出了优化时间复杂度的方法,让我们将注意力转回到本文介绍的 @cache 装饰器。@cache 装饰器的作用是将函数的执行结果缓存,在下次以相同参数调用函数时直接返回上一次的结果,与上面的优化方式完全一致。

所以,只需要在递归函数上加 @cache 装饰器,递归的重复执行就可以解决,时间复杂度就能从 O(2^n) 降为 O(n) 。代码如下:

from functools import cache


@cache
def feibo_recur(n):
    if n < 0:
        return "n小于0无意义"
    if n == 0 or n == 1:
        return 1
    return feibo_recur(n-1) + feibo_recur(n-2)

代码比自己实现更加简洁优雅,并且每次使用时直接加上 @cache 装饰器就行,专注处理业务逻辑。下面看一下实际的运行时间。

for i in [10, 20, 30, 40, 41, 42, 43]:
    start = time.time()
    print(f'第{i}个斐波那契数:', feibo_recur(i))
    end = time.time()
    print(f'n={i} Cost Time: ', end - start)

Output:

第10个斐波那契数: 89
n=10 Cost Time:  0.0
第20个斐波那契数: 10946
n=20 Cost Time:  0.0
第30个斐波那契数: 1346269
n=30 Cost Time:  0.0
第40个斐波那契数: 165580141
n=40 Cost Time:  0.0
第41个斐波那契数: 267914296
n=41 Cost Time:  0.0
第42个斐波那契数: 433494437
n=42 Cost Time:  0.0
第43个斐波那契数: 701408733
n=43 Cost Time:  0.0

运行时间全都降到小数点后很多位了(只显示了 0.0 ),完美解决问题,非常精妙。以后遇到相似的情况,可以直接使用 @cache ,实现“记忆化”的缓存功能。

参考文档:
[1] Python文档标准库functools:https://docs.python.org/zh-cn/3.11/library/functools.html#functools.cache


相关阅读:

详解Python中的三元运算

📢欢迎 点赞👍 收藏⭐ 评论📝 关注 如有错误敬请指正!

☟ 学Python,点击下方名片关注我。☟

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/434054.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL】数据库基础知识

1、数据库简介 1.1 什么是数据库 数据库其实就是"基于数据结构"实现出来的一类软件&#xff0c;这类软件可以用来对数据进行管理&#xff0c;管理也就是对数据进行增删查改等一些操作 为什么说数据库是基于数据结构实现出来的"一类软件"呢&#xff1f; 答&…

本地化部署大语言模型 ChatGLM

本地化部署大语言模型 ChatGLM 本地化部署大语言模型 ChatGLM前期筹备GitHub 基础包语言模型文件基础配置显存查看方法 Anaconda 模块Anaconda 环境创建根目录操作基础依赖加载transformers 和 protobuf 库加载Pytorch 源修改依赖库补充补充依赖 pypi 配置cchardet 依赖错误解决…

开放式耳机好用吗,盘点几款口碑不错的开放式耳机

​开放式耳机作为一种全新的耳机形态&#xff0c;已经成为了当前市场上非常火爆的一款产品。由于无需入耳佩戴&#xff0c;可以很好的避免了耳膜受到损伤&#xff0c;而且也能够让我们在佩戴眼镜时也能够正常使用。加上开放式耳机的音质和舒适度都要优于其他类型的耳机&#xf…

全球上线!ABB中国涡轮增压器分拆 – 数据清理阶段完成

ABB是数字行业的技术前沿者&#xff0c;拥有四项主营业务&#xff1a;电气化&#xff0c;工业自动化&#xff0c;运动控制以及机器人和离散自动化。ABB总部位于瑞士苏黎世&#xff0c;业务遍及100多个国家&#xff0c;拥有约105&#xff0c;000名员工。2021年&#xff0c;该公司…

dockerDesktop依赖wsl,及docker可视化推荐Portainer

也是今天无意中发现的Portainer 之前采用的1panel和宝塔做运维可视化 现在换成dockerPortainer 宝塔和1panel和portranier对比 宝塔广告太多&#xff0c;而且不适合深入了解运维技术 1panel个人感觉无广告颜值很高&#xff0c;但是还是有局限&#xff0c;不适合深入了解运维…

MAC 用 brew安装 mysql并且设置开机自启动

目录 一.安装 mysql 1.安装 mysql 2.启动 mysql 3.关闭 mysql 4.初次安装需要修改 root 密码 5.验证密码 二.设置开机启动 1.找到plist 文件 2.将启动文件 plist 复制到 LaunchAgents目录 3.验证 一.安装 mysql 1.安装 mysql 1.安装默认版可以直接执行安装命令 brew…

上岸美团,我的面经!

作者&#xff1a;阿秀 校招八股文学习网站&#xff1a;https://interviewguide.cn 这是阿秀的第「257」篇原创 小伙伴们大家好&#xff0c;我是阿秀。 欢迎今年参加秋招的小伙伴加入阿秀的学习圈&#xff0c;目前已经超过 2200 小伙伴加入&#xff01;去年认真准备和走下来的基…

GreenPlum (一) 初识

在开始了解GreenPlum之前&#xff0c;应该对这种产品的诞生有基本的了解&#xff0c;搭建一个基本的知识框架。对以下历史有基本了解之后应对下文术语进行基本阅读。 ​ 阅读目标: 阅读完成后需要对相关术语以及greenplum有基础理解。 文案基本互联网相关blog进行整体汇总&…

苹果跌倒检测新专利获得,结合苹果Find My可准确定位

苹果首款“跌倒检测”功能专利可追溯到 2018 年公示的 20190103007&#xff0c;后续又获得了至少 5 项相关专利。根据美国商标和专利局&#xff08;USPTO&#xff09;上周四公示的专利&#xff0c;苹果公司再次获得了一项“跌倒检测”功能专利。 苹果在报告中表示&#xff0c…

亚马逊云科技让你在云端发现企业穿越周期稳健发展的力量

2023年3月29日「哈佛商业评论-未来管理沙龙」活动盛大启幕&#xff0c;此次沙龙活动以穿越周期的力量为主题方向&#xff0c;以解码跨国企业持续增长源动力为主旨&#xff0c;希望为企业高层管理者们带来更多思考和启迪。 作为特邀嘉宾&#xff0c;亚马逊全球副总裁、亚马逊云…

内存的分区

目录 内存分区介绍 区域功能 内存分区运行前后的区别 运行之前&#xff08;代码区数据区未初始化数据区&#xff09; 运行之后&#xff08;代码区数据区未初始化数据区栈区堆区&#xff09; 缓冲区 缓冲区有什么用&#xff1f; 缓冲区的三种类型 缓冲区的刷新 内存分布图 栈与堆…

AI绘画——ControlNet扩展安装教程

目录 1.ControlNet安装 2.预处理模型安装 预处理模型&#xff08;annotator&#xff09;下载链接&#xff1a; 预处理模型安装地址&#xff1a; 3.ControlNet模型下载 Controlnet模型下载地址&#xff1a; Controlnet模型安装目录&#xff1a; 注&#xff1a;&…

KDZRS-40A三通道变压器直流电阻测试仪

一、产品概述 变压器绕组的直流电阻测试是变压器在交接、大修和改变分接开关后的试验项目。在通常情况下&#xff0c;用传统的方法&#xff08;电桥法和压降法&#xff09;测量变压器绕组以及大功率电感设备&#xff08;发电机&#xff09;的直流电阻是一项费时费工的工作。为了…

VMware从零配置安装CentOS 7

不跳步图文详细安装教程 一、VMware的下载二、VMware的安装三、CentOS7的下载第一步&#xff1a;根据自己电脑操作系统的位数点击选择&#xff08;大多数都是64位操作系统&#xff09;第二步&#xff1a;任意挑选一个镜像源进入下载界面第三步&#xff1a;下载对应版本的CentOS…

比较运算符、关键字子查询MySQL数据库 (头歌实践教学平台)

文章目的初衷是希望学习笔记分享给更多的伙伴&#xff0c;并无盈利目的&#xff0c;尊重版权&#xff0c;如有侵犯&#xff0c;请官方工作人员联系博主谢谢。 目录 第1关&#xff1a;带比较运算符的子查询 任务描述 相关知识 子查询 带比较运算符的子查询 编程要求 第2关…

Spring Messaging-远程命令执行漏洞(CVE-2018-1270)

Spring Messaging-远程命令执行漏洞&#xff08;CVE-2018-1270&#xff09; 0x00 前言 spring messaging为spring框架提供消息支持&#xff0c;其上层协议是STOMP&#xff0c;底层通信基于SockJS&#xff0c;在spring messaging中&#xff0c;其允许客户端订阅消息&#xff0…

微服务 - Redis缓存 · 数据结构 · 持久化 · 分布式 · 高并发

一、分布式解决 Session 的问题 在单站点中&#xff0c;可以将在线用户信息存储在Session中&#xff0c;随时变更获取信息&#xff1b;在多站点分布式集群如何做到Session共享呢&#xff1f;架设一个Session服务&#xff0c;供多服务使用。 频繁使用的数据存在DB端&#xff0…

向隐形冠军学习:聚焦人效,用时间管理提效益

注&#xff1a; 本文来源于盖雅工场联合创始人兼CEO 章新波 在2023狮山论坛“ 向隐形冠军学习&#xff1a; 聚焦人效&#xff0c;用时间管理提效益 ”的主题分享。 文&#xff5c;章新波 整理 &#xff5c;盖雅学苑 在人力资源行业以及各大企业&#xff0c;「人效」这个词…

How to use CCS to debug a running M4F core that was started by Linux?

参考FAQ:AM62x & AM64x: How to use CCS to debug a running M4F core that was started by Linux? 问题记录&#xff1a; 1.使用SD卡启动模式&#xff0c;板上运行Linux。 当Linux系统启动后&#xff0c;9表示M4F core&#xff1a; am64xx-evm login: root rootam64xx…

Maven 打包跳过测试的 5 种方法

平时开发时的工作的话之主要负责写代码就行了&#xff0c;什么发布项目啊&#xff0c;好吧不是我们干的事。在我们的了解中打包发布项目应该不是一个困难的问题。 对&#xff0c;最简单的方法就行使用直接使用maven插件打包&#xff0c;甚至我们都不需要知道他是怎么实现的&am…