【python】Python报错:RecursionError: maximum recursion depth exceeded in comparison-程序员宅基地

技术标签: python  Python  开发语言  

【python】Python报错:RecursionError: maximum recursion depth exceeded in comparison

引出问题

1. 错误

今天在用python写一个递归查询数据库的程序时,报了一个错误:

RecursionError: maximum recursion depth exceeded in comparison

错误的大致意思就是递归超过了最大的深度。

2. 原因

查询过相关文档和资料后才发现了问题原因,python的递归深度是有限制的,默认为1000。当递归深度超过1000时,就会报错。

3. 解决办法

可以将递归的深度修改的大一些,即可解决问题,但是还是建议在程序中不要使用太深的递归层数。

import sys
sys.setrecursionlimit(100000) #例如这里设置为十万 

4. 补充测试

由于对最大递归层数产生兴趣,于是我在自己电脑上用以下代码做了测试:

def recursion(depth):
    depth += 1
    print(depth)
    recursion(depth)

recursion(0)

在这里插入图片描述

5. 补充测试2

修改最大递归层数为100000后,再执行上面的代码,发现最多也只是可以递归到第3220层
系统为了保证自己不会内存溢出,把它关了。要不然就是Python为了保证电脑不会****,把自己关了.(猜测)

Python递归优化方法

递归栈溢出

Python的递归调用栈的深度有限制,默认深度为998,可以通过sys.getrecursionlimit()查看。

针对递归栈溢出,我们可以将默认深度设置为大一些,这样不会报错,但是再大的深度总归是有限的,而且深度越大对内存的占用也就越大,这对我们的程序是不利的。所以一般情况下我们不要将栈的深度设定太大。

但有时候我们又需要无限但递归,这里我们就可以用到尾递归。

尾递归

  • 尾递归在很多语言中都可以被编译器优化, 基本都是直接复用旧的执行栈, 不用再创建新的栈帧, 原理上其实也很简单, 因为尾递归在本质上看的话递归调用是整个子过程调用的最后执行语句, 所以之前的栈帧的内容已经不再需要, 完全可以被复用。

  • 需要注意的是, 一定记住尾递归的特点: 递归调用是整个子过程调用的最后一步,return的时候不能出现计算。否则就不是真正的尾递归了, 如下就不是真正的尾递归, 虽然递归调用出现在尾部

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

很明显递归调用并不是整个计算过程的最后一步, 计算fib(n)是需要先递归求得fib(n-1)和fib(n-2), 然后做一步加法才能得到最终的结果。
如下是尾递归

def fib(n, a, b):
    if n == 1:
        return a
    else:
        return fib(n-1, b, a+b)

然而!!!Python语言的编译器是不支持尾递归的!!!

上面那串红字是什么意思呢,即使你用了尾递归的语法写了一串递归的代码,但是最后还是会报深度问题的错,因为python的源码中并没有集成对尾递归的支持。。。

怎么办呢?有几种解决办法:

  1. 修改源码,如果你不怕会有后续错误的话。。
  2. 将上述代码最后的return改为yield,然后在调用的时候用next,利用生成器实现。(会出一个问题,如果递归的函数需要传参,而参数是会变化的话,你会发现每次调用参数都不会变。。)
  3. 往下看~~

关于Python中的尾递归调用有一段神奇的代码:

import sys

class TailCallException(BaseException):
    def __init__(self, args, kwargs):
        self.args = args
        self.kwargs = kwargs

def tail_call_optimized(func):
    def _wrapper(*args, **kwargs):
        f = sys._getframe()
        if f.f_back and f.f_back.f_back and f.f_code == f.f_back.f_back.f_code:
            raise TailCallException(args, kwargs)

        else:
            while True:
                try:
                    return func(*args, **kwargs)
                except TailCallException, e:
                    args = e.args
                    kwargs = e.kwargs
    return _wrapper



@tail_call_optimized
def fib(n, a, b):
    if n == 1:
        return a
    else:
        return fib(n-1, b, a+b)

r = fib(1200, 0, 1) #不报错!突破了调用栈的深度限制!只要加上装饰器,尾递归就实现了!!

嗯,没错,就是这么简单。以后要想实现尾递归都时候就复制上面装饰器以上都代码,然后将递归函数加上该装饰器就OK。
 
以上的代码是怎样的工作的呢?

理解它需要对Python虚拟机的函数调用有一定的理解。其实以上代码和其他语言对尾递归的调用的优化原理都是相似的,那就是在尾递归调用的时候重复使用旧的栈帧, 因为之前说过, 尾递归本身在调用过程中, 旧的栈帧里面那些内容已经没有用了, 所以可以被复用。

Python的函数调用首先要了解code objectfunction objectframe object这三个object(对象),

  • code object是静态的概念, 是对一个可执行的代码块的抽象, module, function, class等等都会被生成code object, 这个对象的属性包含了”编译器”(Python是解释型的,此处的编译器准确来说只是编译生成字节码的)对代码的静态分析的结果, 包含字节码指令, 常量表, 符号表等等。

  • function object是函数对象, 函数是第一类对象, 说的就是这个对象。当解释器执行到def fib(…)语句的时候(MAKE_FUNCTION), 就会基于code object生成对应的function object。但是生成function object并没有执行它, 当真正执行函数调用的时候, fib(…)这时候对应的字节码指令(CALL_FUNCITON), 可以看一下, CPython的源码, 真正执行的时候Python虚拟机会模拟x86CPU执行指令的大致结构, 而运行时栈帧的抽象就是frame obejct, 这玩意儿就模拟了类似C里面运行时栈, 寄存器等等运行时状态, 当函数内部又有函数调用的时候, 则又会针对内部的嵌套的函数调用生成对应的frame object, 这样看上去整个虚拟机就是一个栈帧连着又一个栈帧, 类似一个链表, 当前栈帧通过f_back这个指针指向上一栈帧, 这样你才能在执行完毕, 退出当前帧的时候回退到上一帧。和C里执行栈的增长退出模式很像。

  • frame object栈帧对象只有在当前函数执行的时候才会产生, 所以你只能在函数内通过sys._getframe()调用来获取当前执行帧对象。通过f.f_back获取上一帧, f.f_back.f_back来获取当前帧的上一帧的上一帧(当前帧的“爷爷”)。

另外一个需要注意到的是, 对于任何对尾递归而言, 其执行过程可以线性展开, 此时你会发现, 最终结果的产生完全可以从任意中间状态开始计算, 最终都能得到同样的执行结果。如果把函数参数看作状态(state_N)的话, 也就是tail_call(state_N)->tail_call(state_N-1)->tail_call(state_N-2)->…->tail_call(state_0), state_0是递归临界条件, 也就是递归收敛的最终状态, 而你在执行过程中, 从任一起始状态(state_N)到收敛状态(state_0)的中间状态state_x开始递归, 都可以得到同样的结果。

当Python执行过程中发生异常(错误)时(或者也可以直接手动抛出raise …), 该异常会从当前栈帧开始向旧的执行栈帧传递, 直到有一个旧的栈帧捕获这个异常, 而该栈帧之后(比它更新的栈帧)的栈帧就被回收了。

有了以上的理论基础, 就能理解之前代码的逻辑了:

  1. 尾递归函数fib被tail_call_optimized装饰, 则fib这个名字实际所指的function object变成了tail_call_optimized里return的_wrapper, fib 指向_wrapper。

  2. 注意_wrapper里return func(*args, **kwargs)这句, 这个func还是未被tail_call_optimized装饰的fib(装饰器的基本原理), func是实际的fib, 我们称之为real_fib。

  3. 当执行fib(1200, 0, 1)时, 实际是执行_wrapper的逻辑, 获取帧对象也是_wrapper对应的, 我们称之为frame_wapper。

  4. 由于我们是第一次调用, 所以”if f.f_back and f.f_back.f_back and f.f_code == f.f_back.f_back.f_code”这句里f.f_code==f.f_back.f_back.f_code显然不满足。

  5. 继续走循环, 内部调用func(*args, **kwargs), 之前说过这个func是没被装饰器装饰的fib, 也就是real_fib。

  6. 由于是函数调用, 所以虚拟机会创建real_fib的栈帧, 我们称之为frame_real_fib, 然后执行real_fib里的代码, 此时当前线程内的栈帧链表按从旧到新依次为: 旧的虚拟机栈帧,frame_wrapper,frame_real_fib(当前执行帧)

real_fib里的逻辑会走return fib(n-1, b, a+b), 有一个嵌套调用, 此时的fib是谁呢?此时的fib就是我们的_wrapper, 因为我们第一步说过, fib这个名字已经指向了_wrapper这个函数对象。

  1. 依然是函数调用的一套, 创建执行栈帧, 我们称之为frame_wrapper2, 注意: 执行栈帧是动态生成的, 虽然对应的是同样函数对象(_wrapper), 但依然是不同的栈帧对象, 所以称之为frame_wrapper2。 今后进入

  2. frame_wrapper2执行, 注意此时的虚拟机的运行时栈帧的结构按从旧到新为:
    旧的虚拟机栈帧、frame_wrapper、frame_real_fib、frame_wrapper2(当前执行栈帧)

  3. 进入frame_wrapper2执行后, 首先获取当前执行帧, 即frame_wrapper2, 紧接着, 执行判断, 此时:

if f.f_back and f.f_back.f_back and f.f_code == f.f_back.f_back.f_code

以上这句就满足了, f.f_code是当前帧frame_wrapper2的执行帧的code对象, f.f_back.f_back.f_code从当前的执行帧链表来看是frame_wrapper的执行帧的code对象, 很显然他们都是同一个code块的code object(def _wrapper……)。于是抛出异常, 通过异常的方式, 把传过来的参数保留, 然后, 异常向旧的栈帧传递, 直到被捕获, 而之后的栈帧被回收, 即抛出异常后, 直到被捕获时, 虚拟机内的执行帧是:旧的虚拟机栈帧、frame_wrapper(当前执行帧)

于是现在恢复执行frame_wrapper这个帧, 直接顺序执行了, 由于是个循环, 同时参数通过异常的方式被捕获, 所以又进入了return func(*args, **kwargs)这句, 根据我们之前说的, 尾递归从递归过程中任意中间状态都可以收敛到最终状态, 所以就这样, 执行两个帧, 搞出中间状态, 然后抛异常, 回收两个帧, 这样一直循环直到求出最终结果。

在整个递归过程中, 没有频繁的递归一次, 生成一个帧, 如果你不用这个优化, 可能你递归1000次, 就要生成1000个栈帧, 一旦达到递归栈的深度限制, 就挂了。

使用了这个装饰器之后, 最多生成3个帧, 随后就被回收了, 所以是不可能达到递归栈的深度的限制的。

注意: 这个装饰器只能针对尾递归使用

参考链接:

1、Python报错:RecursionError: maximum recursion depth exceeded in comparison

2、Python递归优化方法

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43331089/article/details/125680834

智能推荐

java 实现 数据库备份_java数据备份-程序员宅基地

文章浏览阅读1k次。数据库备份的方法第一种:使用mysqldump结合exec函数进行数据库备份操作。第二种:使用php+mysql+header函数进行数据库备份和下载操作。下面 java 实现数据库备份的方法就是第一种首先我们得知道一些mysqldump的数据库备份语句备份一个数据库格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 --database 数据库名 ..._java数据备份

window10_ffmpeg调试环境搭建-编译64位_win10如何使用mingw64编译ffmpeg-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏14次。window10_ffmpeg调试环境搭建_win10如何使用mingw64编译ffmpeg

《考试脑科学》_考试脑科学pdf百度网盘下载-程序员宅基地

文章浏览阅读6.3k次,点赞9次,收藏14次。给大家推荐《考试脑科学》这本书。作者介绍:池谷裕二,日本东京大学药学系研究科教授,脑科学研究者。1970年生于日本静冈县,1998年取得日本东京大学药学博士学位,2002年起担任美国哥伦比亚大学客座研究员。专业为神经科学与药理学,研究领域为人脑海马体与大脑皮质层的可塑性。现为东京大学药学研究所教授,同时担任日本脑信息通信融合研究中心研究主任,日本药理学会学术评议员、ERATO人脑与AI融合项目负责人。2008年获得日本文部大臣表彰青年科学家奖,2013年获得日本学士院学术奖励奖。这本书作者用非常通俗易懂_考试脑科学pdf百度网盘下载

今天给大家介绍一下华为智选手机与华为手机的区别_华为智选手机和华为手机的区别-程序员宅基地

文章浏览阅读1.4k次。其中,成都鼎桥通信技术有限公司是一家专业从事移动通讯终端产品研发和生产的高科技企业,其发布的TD Tech M40也是华为智选手机系列中的重要代表之一。华为智选手机是由华为品牌方与其他公司合作推出的手机产品,虽然其机身上没有“华为”标识,但是其品质和技术水平都是由华为来保证的。总之,华为智选手机是由华为品牌方和其他公司合作推出的手机产品,虽然外观上没有“华为”标识,但其品质和技术水平都是由华为来保证的。华为智选手机采用了多种处理器品牌,以满足不同用户的需求,同时也可以享受到华为全国联保的服务。_华为智选手机和华为手机的区别

c++求n个数中的最大值_n个数中最大的那个数在哪里?输出其位置,若有多个最大数则都要输出。-程序员宅基地

文章浏览阅读7.6k次,点赞6次,收藏17次。目录题目描述输入输出代码打擂法数组排序任意输入n个整数,把它们的最大值求出来.输入只有一行,包括一个整数n(1_n个数中最大的那个数在哪里?输出其位置,若有多个最大数则都要输出。

python overflowerror_python – 是否真的引发了OverflowError?-程序员宅基地

文章浏览阅读520次。Python 2.7.2 (v2.7.2:8527427914a2, Jun 11 2011, 15:22:34)[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwinType "help", "copyright", "credits" or "license" for more information.>>> float(1...

随便推点

MySQL基础命令_mysql -u user-程序员宅基地

文章浏览阅读4.1k次。1.登录MYSQL系统命令打开DOS命令框shengfen,以管理员的身份运行命令1:mysql -u usernae -p password命令2:mysql -u username -p password -h 需要连接的mysql主机名(localhost本地主机名)或是mysql的ip地址(默认为:127.0.0.1)-P 端口号(默认:3306端口)使用其中任意一个就OK,输入命令后DOS命令框得到mysql>就说明已经进入了mysql系统2. 查看mysql当中的._mysql -u user

LVS+Keepalived使用总结_this is the redundant configuration for lvs + keep-程序员宅基地

文章浏览阅读484次。一、lvs简介和推荐阅读的资料二、lvs和keepalived的安装三、LVS VS/DR模式搭建四、LVS VS/TUN模式搭建五、LVS VS/NAT模式搭建六、keepalived多种real server健康检测实例七、lvs持久性工作原理和配置八、lvs数据监控九、lvs+keepalived故障排除一、LVS简介和推荐阅读的资料 学习LVS+Keepalived必须阅读的三个文档。1、 《Keepalived权威指南》下载见http://..._this is the redundant configuration for lvs + keepalived server itself

Android面试官,面试时总喜欢挖基础坑,整理了26道面试题牢固你基础!(3)-程序员宅基地

文章浏览阅读795次,点赞20次,收藏15次。AIDL是使用bind机制来工作。java原生参数Stringparcelablelist & map 元素 需要支持AIDL其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。下图是我进阶学习所积累的历年腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

机器学习-数学基础02补充_李孟_新浪博客-程序员宅基地

文章浏览阅读248次。承接:数据基础02

短沟道效应 & 窄宽度效应 short channel effects & narrow width effects-程序员宅基地

文章浏览阅读2.8w次,点赞14次,收藏88次。文章目录1. 概念:Narrow Width Effect: 窄宽度效应Short Channel effects:短沟道效应阈值电压 (Threshold voltage)2. 阈值电压与沟道长和沟道宽的关系:Narrow channel 窄沟的分析Short channel 短沟的分析1. 概念:Narrow Width Effect: 窄宽度效应在CMOS器件工艺中,器件的阈值电压Vth 随着沟道宽度的变窄而增大,即窄宽度效应;目前,由于浅沟道隔离工艺的应用,器件的阈值电压 Vth 随着沟道宽度_短沟道效应

小米组织架构再调整,王川调职,雷军自任中国区总裁_小米更换硬件负责人-程序员宅基地

文章浏览阅读335次。5月17日,小米集团再发组织架构调整及任命通知。新通知主要内容为前小米中国区负责人王川调职,雷军自任中国区总裁。小米频繁调整背后,雷军有些着急了中国区手机业务持续下滑。根据IDC最近公布的数据,小米一季度全球出货量为2750万台,相比去年同期的2780万台,小幅下降。参考Canalys、Counterpoint的统计,小米一季度出货量也都录得1%的同比下滑。作为对比,IDC数据显示,华为同期出..._小米更换硬件负责人