63、Python之函数高级:装饰器缓存实战,优化递归函数的性能

news2025/1/8 4:34:41

引言

通过前面的文章,我们已经掌握了Python中常用的装饰器的使用技巧,这篇文章中,我们通过一个装饰器的实战案例,来进一步加深对装饰器的适用场景的理解。

本文的主要内容有:

1、递归函数

2、递归实现斐波那契数列的计算

3、递归函数的性能优化

递归函数

相信稍微接触过编程的同学,一定都听过“递归”。所谓“递归函数(Recursive Function)”是一种在定义过程中调用自身的函数。递归是一种很自然的解决问题的有效编程技术,特别适用于那些大规模的计算问题可以分解为相同问题,只是规模更小的子问题的情况。

一个递归函数,必须包含两个组成部分:

1、基准情况(Base Case):这是递归的终止条件,确保递归不会无限进行。当基准条件满足时,函数返回一个具体的值,而不再是函数的递归调用。

2、递归情况(Recursive Case):这是函数调用自身的情况,通常是将问题规模不断减小的情况下进行递归调用。

所以,一个极简的递归函数的写法大概如下:

e1066db87a0f838fa0986dd86ffcdf82.jpeg

递归函数通常有3个特性,需要注意:

1、简洁性:递归函数的代码实现往往会比循环、迭代的实现更加简洁,而且可读性更高,特别是对于树结构和图结构等复杂的数据结构。

2、分而治之:递归思想,天然适用于这种分而治之的策略,大事化小,问题不变,规模不断缩小。

3、堆栈使用:由于每次递归调用都会在堆栈中创建一个新的函数调用,该函数使用的栈的深度会不断增加,因此,深度过大的递归可能导致堆栈溢出(Stack Overflow)。

为了避免过深的堆栈,导致内存溢出等问题,Python中会有一个默认的递归限制,我们可以通过sys模块获取递归调用栈的最大深度。

import sys

print(f'递归调用的最大栈深:{sys.getrecursionlimit()}')

执行结果:

065a07709fe03ebe5eb405cc08c727a8.jpeg

可以看到,在笔者的运行环境中,当前的最大栈深默认为1000,也就意味着,超过1000的会被自动终止。

我们试着写一个“忘记”写基准情况的无限递归,来验证下栈深:

stack_depth = 0


def infinite_recursive():
    global stack_depth
    stack_depth += 1
    print(f'当前栈深:{stack_depth}')
    return infinite_recursive()


infinite_recursive()

执行结果:

6165fab781c490428c70412d2f5ff03b.jpeg

可以看到,实际执行时,由于其他代码的执行也会占用,实际的栈深会略小于栈深的默认限制。

递归实现斐波那契数列的计算

已经了解了递归函数的用法,我们通过递归函数来实现一个斐波那契数列的计算的需求。

简单介绍一下所谓的斐波那契数列:

斐波那契数列(Fibonacci Sequence)是一个在数学和自然界中广泛存在的有趣的数列,以意大利数学家斐波那契的名字命名。满足如下规律:

1、数列的前两个数字通常定义为0和1(有时也使用两个1作为开始)。

2、从第三个数字开始,每个数都是前两个数字之和。

斐波那契数列不仅在数学中有各种应用,在自然现象中也有,比如:植物的叶序、花瓣的排列等,此外,这个数列还与黄金比例(1.618)有着密切的关系——当数列趋于无穷时,相邻两个数的比值将接近于黄金比例。

我们通过递归函数来实现求斐波那契数列中任意位置的数字:

def fibonacci(n):
    if n == 1:
        return 0
    if n == 2:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)


if __name__ == '__main__':
    for i in range(1, 30):
        print(fibonacci(i), end=' ')

运行结果:

083f7e5f89b4ef925a03af76d2371dfa.jpeg

递归函数的性能优化

递归相对于迭代,代码会更加简洁、可读。但是,很多事情都是有利有弊的。递归算法的最大问题在于性能的开销,尤其当递归的深度较大时,或者递归算法本身存在的大量的重复计算。

我们上面的斐波那契数列的递归函数就是存在大量的重复计算,导致参数稍微大点,就会要大量的等待时间。

比如:

import time


def fibonacci(n):
    if n == 1:
        return 0
    if n == 2:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)


if __name__ == '__main__':
    for i in range(30, 40):
        start = time.time()
        res = fibonacci(i)
        end = time.time()
        print(f"Fib({i}) = {res} 耗时:{round(end -start, 2)} 秒")

执行结果:

0be70dc4f854e1901634838ffed8fa7c.jpeg

从计算结果可以看到,超过30时,就已经有很明显的延迟了。

其实,对递归函数的性能优化,本质上都是尽量消除重复计算,可以通过转换为迭代、使用缓存等。

我们首先看常规的优化策略:

优化方法1:

import time


def fibonacci(n):
    if n == 1:
        return 0
    if n == 2:
        return 1
    fib = [0] * (n + 1)
    fib[1] = 0
    fib[2] = 1
    for i in range(3, n + 1):
        fib[i] = fib[i - 1] + fib[i - 2]
    return fib[n]


if __name__ == '__main__':
    for i in range(30, 40):
        start = time.time()
        res = fibonacci(i)
        end = time.time()
        print(f"Fib({i}) = {res} 耗时:{round(end - start, 4)} 秒")

先看效果:

799a51b3a9792e5d5d62e020b7e8383d.jpeg

核心优化点在于:

53165169a67858d854c42023cb7b65c9.jpeg

秒出,哪怕保留了4位小数,也是0秒,典型的以空间换时间的做法,而且避免了重复计算,直接从列表中取现成的就好了。所以,计算速度快了很多,几乎感知不到延迟。

效果很明显,但是,代码改动的逻辑比较大,而且,不能复用。

另一种优化方法,就是我们一直在用的装饰器了。

优化方法2:

import time


def cache(func):
    mem = {}

    def wrap(*args):
        res = mem.get(args)
        if not res:
            res = mem[args] = func(*args)
        return res

    return wrap


@cache
def fibonacci(n):
    if n == 1:
        return 0
    if n == 2:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)


if __name__ == '__main__':
    for i in range(30, 40):
        start = time.time()
        res = fibonacci(i)
        end = time.time()
        print(f"Fib({i}) = {res} 耗时:{round(end - start, 4)} 秒")

执行结果:

62f498bf868366bf49a181007f4a28f9.jpeg

核心优化逻辑:

41958455629c0f85cf167c40610777d0.jpeg

使用装饰器的优化方法,原函数无需做任何修改,可读性仍然较好,性能同样得到了提升,秒出。

而且,装饰器的实现方法,更加具备通用性,这个装饰器可以应用到其他需要缓存子问题计算结果的情况,代码的复用性更加。

总结

本文简单介绍了递归函数的概念以及递归函数的特性,然后看到了递归函数最大的问题是大量重复计算导致的性能较差的问题。最后,我们通过两个方法实现了对递归函数的性能的优化,优化的核心在于以空间换时间,规避大量的重复计算。

7ac687916cc8e13352c4e8a615d6976a.jpeg

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

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

相关文章

AWTK HTML View 控件更新

AWTK HTML View 控件基于 Lite HTML 实现,从最初的版本开始,3 年多过去了,Lite HTML 做了大量的更新,最近抽空将 AWTK HTML View 控件适配到最新版本的 Lite HTML,欢迎大家使用。 AWTK HTML View 控件。HTML View 控件…

SAP B1 基础实操 - 用户定义字段 (UDF)

目录 一、功能介绍 1. 使用场景 2. 操作逻辑 3. 常用定义部分 3.1 主数据 3.2 营销单据 4. 字段设置表单 4.1 字段基础信息 4.2 不同类详细设置 4.3 默认值/必填 二、案例 1 要求 2 操作步骤 一、功能介绍 1. 使用场景 在实施过程中,经常会碰见用户需…

Qt线程使用

嗨嗨嗨,今天又学到了新的知识——线程,这个玩意在项目中使用的频率是非常高的,毕竟电脑的主线程就那么一个,那么这也就是我们为啥要学习线程的原因。比如说,我们们的游戏,如果我们的游戏界面显示的同时我们…

【生日视频制作】奥迪A8提车交车仪式AE模板修改文字软件生成器教程特效素材【AE模板】

奥迪A8提车交车仪式AE模板制作过程软件生成器素材 AE模板套用改图文教程↓↓: 怎么如何做的【生日视频制作】奥迪A8提车交车仪式AE模板修改文字软件生成器教程特效素材【AE模板】 生日视频制作步骤: 安装AE软件 下载AE模板 把AE模板导入AE软件 修改图片…

PD快充协议方案 及应用场景

快充协议诱骗原理主要依赖于快充协议芯片与供电端(如PD充电器)之间的握手通信,以申请所需要的电压与电流,确保充电过程安全、快速且高效。这种芯片通过内置的通讯模块与供电端通信,根据设备的实际需求调整输出电压和电…

大路灯护眼灯有必要吗安全吗?性价比高落地护眼灯推荐

大路灯护眼灯有必要吗安全吗?近几年来,随着生活节奏的加快,目前青少年的近视率呈现一个直线上升的趋势,其中占比达到了70%以上,并且最令人意外的是小学生竟然也占着比较大的比重,这一系列的数据不仅表明着近…

苍穹外卖学习笔记(一)

文章目录 开发环境搭建一. 前端环境搭建二. 后端环境搭建1.进入idea项目2.提交git仓库(推送github远程仓库)3.数据库环境搭建4.前后端联调(在源代码中项目已经实现登录功能)nginx反向代理好处: 三. 完善登录功能(md5加密存储)1.首先打开pojo模块中实体类的employee,…

[STL --stack_queue详解]stack、queue,deque,priority_queue,容器适配器

stack stack介绍 1、stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。 2、stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供…

原理图库和PCB库的命名规范及创建封装、使用封装管理器

原理图库 命名规范 原理图中元件值标注规则 注:元件值(Component Value)就是元件最主要的特征对应的值。 Component value. Most analog components have a value that must be specified by this field (e.g., 2.7 kΩ). Additional disti…

c++数据结构之队列

目录 一、队列的含义 1.队列的使用 2.队列的结构 二、顺序队列的实现 1.队列的定义 2.队列的初始化 3.清空对列 4.队列是否为空 5.获取队列的长度 6.获取头元素的值 7.入队列 8.出队列 9.遍历队列中的值 10.总代码 11.打印结果 三、链表队列的实现 1.队列的…

【Hot100】LeetCode—347. 前 K 个高频元素

目录 1- 思路自定义Node结点 哈希表实现 2- 实现⭐347. 前 K 个高频元素——题解思路 3- ACM实现 原题连接:347. 前 K 个高频元素 1- 思路 自定义Node结点 哈希表实现 ① 自定义 Node 结点: 自定义 Node 结点中有 value 和 cnt 字段,其中…

力扣接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 示例 1: 输入:height [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表…

html css网页制作

​ 大家好,我是程序员小羊! 前言: HTML 和 CSS 是制作网页的基础。HTML 用于定义网页的结构和内容,CSS 用于设计网页的样式和布局。以下是一个详细的网页制作成品教程,包括 HTML 和 CSS 的基础知识,及如何…

MySQL基础(7)- 多表查询

目录 一、笛卡尔积的错误与正确的多表查询 1.出现笛卡尔积错误 2.正确的多表查询:需要有连接条件 3.查询多个表中都存在的字段 4.SELECT和WHERE中使用表的别名 二、等值连接vs非等值连接、自连接vs非自连接 1.等值连接 vs 非等值连接 2.自连接 vs 非自连…

安卓逆向(之)真机root(红米手机)

概览: 1, 手机解锁 2, 下载官方系统包,推荐线刷包,取出镜像文件 3, magisk工具修补 官方系统包 4, adb:命令对手机刷 root 5, 完成 6, 小米手机解锁 点击 小米手机解锁OEM官方教程 记得数据线连接手机电脑 工具下载 点击 下载adb(电脑操作…

进程间通信-进程池

目录 理解​ 完整代码 完善代码 回收子进程&#xff1a;​ 不回收子进程&#xff1a; 子进程使用重定向优化 理解 #include <iostream> #include <unistd.h> #include <string> #include <vector> #include <sys/types.h>void work(int rfd) {…

Windows下使用cmake编译OpenCV

Windows下使用cmake编译OpenCV cmake下载OpenCV下载编译OpenCV cmake下载 下载地址&#xff1a;https://cmake.org/download/ 下载完成&#xff0c;点击选择路径安装即可 OpenCV下载 下载地址&#xff1a;https://github.com/opencv/opencv/releases/tag/4.8.1因为我们是编译…

2024软件测试需要具备的技能(软技能硬技能)

软件测试的必备技能 在往期的文章分享了很多的面试题&#xff0c;索性做一个转型。从零基础开始讲解&#xff0c;结合面试题来和大家一起学习交流软件测试的艺术。 第一个是专业技能&#xff0c;也叫硬技能。 第二个叫做软技能。 我们在上一篇文章中讲到了软件测试流程的5个…

ChatGPT在论文写作领域的应用:初稿设计

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 学术论文写作中&#xff0c;内容清晰、结构合理的初稿至关重要。通过 ChatGPT&#xff0c;写作者可以快速生成内容框架、明确研究问题&#xff0c;并优化表达方式。不仅提高了写作效率&…

笔记整理—内核!启动!—kernel部分(1)驱动与内核的关系

首先&#xff0c;恭喜完成了uboot部分的内容整理&#xff0c;其次补充一点&#xff0c;uboot第一部分和第二部分的工作不是一定的&#xff0c;在不同的版本中&#xff0c;可能这个初始化早一点&#xff0c;那个的又放在了第二部分&#xff0c;版本不同&#xff0c;造成的工作顺…