【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 += 1print(depth)recursion(depth)recursion(0)

5. 补充测试2

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

Python递归优化方法

递归栈溢出

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

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

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

尾递归

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

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

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

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

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

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

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

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

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

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

import sysclass TailCallException(BaseException):def __init__(self, args, kwargs):self.args = argsself.kwargs = kwargsdef 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.argskwargs = e.kwargsreturn _wrapper@tail_call_optimized
def fib(n, a, b):if n == 1:return aelse: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递归优化方法

【python】Python报错:RecursionError: maximum recursion depth exceeded in comparison相关推荐

  1. 解决报错RecursionError: maximum recursion depth exceeded in comparison

    发现python默认的递归深度是很有限的(默认是1000),因此当递归深度超过999的样子,就会引发这样的一个异常. 解决方案: 可以修改递归深度的值,让它变大大一点 import sys sys.s ...

  2. python递归报错 RuntimeError: maximum recursion depth exceeded

    递归是我们常用的一种编程方法,通俗的说就是样一个方法自己调用自己. 今天写爬虫的时候,使用递归出现如下错误 RuntimeError: maximum recursion depth exceeded ...

  3. python编译器报错:“RecursionError: maximum recursion depth exceeded in comparison”解决方案

    python编译器报错:"RecursionError: maximum recursion depth exceeded in comparison"解决方案 在使用递归迭代语句 ...

  4. python递归深度报错--RuntimeError: maximum recursion depth exceeded

    代码上传至https://github.com/gatieme/AderXCoding/blob/master/python/error/depth-exceeded.py 问题 这段时间用Pytho ...

  5. Python:pyinstaller报错【A RecursionError maximum recursion depth exceeded occurred】

    pyinstaller demo.py时出现如下问题: ============================================================= A Recursio ...

  6. RecursionError: maximum recursion depth exceeded

    pyinstaller打包报错: RecursionError: maximum recursion depth exceeded 放开那禽兽冲我来 2018-07-13 14:53:41  1036 ...

  7. 总结的若干关于RecursionError: maximum recursion depth exceeded问题的解决办法

    情形一: pyinstaller打包时遇到, RecursionError: maximum recursion depth exceeded是递归错误,大概率是自己调用自己太多次导致的. 解法一: ...

  8. [Previous line repeated 995 more times]RecursionError: maximum recursion depth exceeded

    原因:超过递归深度 解决: import sys sys.setrecursionlimit(100000) 又遇到新问题:Process finished with exit code -10737 ...

  9. Python maximum recursion depth exceeded while calling a Python object (gevent的SSL无限递归错误)的问题解决

    报错信息 源码位置 分析 很尴尬,完全看不出原因导致这个报错 解决方法 通过删除代码的方式一部一部删除,找到了问题出处 原因是包的顺序出现了问题,把位置互换一下,发现没有报错了,但是很明确的告诉你这两 ...

  10. np.argwhere报错maximum recursion depth exceeded while calling

    完整的报错是:RecursionError: maximum recursion depth exceeded while calling a Python object 解决方法 建议根据代码逻辑修 ...

最新文章

  1. mqtt android简书,iOS MQTT协议笔记
  2. Python爬虫-HTMLSession的使用
  3. jmeter接口测试实例-关联
  4. CDI中的事务异常处理
  5. session 学习
  6. 如何使用Alert 组件
  7. 零基础转行web前端,如何高效的去学习web前端?
  8. oracle bookauthor,Oracle 聚簇(征集)
  9. WebHubBot 网络爬虫
  10. Python进程池使用
  11. 两层循环的中断,注意中断退出
  12. 西威变频器 服务器显示,西威变频器故障查询及操作方法
  13. 关于oracle端口映射的远程连接
  14. VM虚拟机的安装及安装操作系统
  15. 有什么数学题库软件吗?4款学生必备APP,题库超全超好用!
  16. android re卸载程序,手机自带软件卸载不了?教你2种方法,强制卸载预装应用程序!...
  17. 批量ping指定端口,批量测试IP地址是否通
  18. 适合运动的无线蓝牙耳机有哪些,运动无线蓝牙耳机推荐
  19. pytorch-词嵌入基础
  20. ln火线零线_ln线哪个是火线零线

热门文章

  1. Gromacs 中文手册目录
  2. adjacent angle_adjacent angle是什么意思_adjacent angle怎么读_adjacent angle翻译_用法_发音_词组_同反义词_邻角-新东方在线英语词典...
  3. 创维电视显示无服务器,常见创维电视机故障及维修方法【详解】
  4. [課程筆記] 機器學習2021(李弘毅) L13. Transformer (下)
  5. IDEA代码文件导航-Navigate使用技巧
  6. 海致大数据京信_2018华为全联接大会|海致网聚提出公安大数据个人计算新理念...
  7. pandas系列学习(七):数据透视表
  8. 力扣LCP3机器人大冒险
  9. 趋势追踪交易课堂:复盘的意义和方法
  10. u盘无法打开 计算机限制,u盘无法打开,教您U盘打不开常用修复方法