递归栈溢出

  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 object、function object、frame 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。 今后进入frame_wrapper2执行, 注意此时的虚拟机的运行时栈帧的结构按从旧到新为:
                     旧的虚拟机栈帧、frame_wrapper、frame_real_fib、frame_wrapper2(当前执行栈帧)

  2. 进入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个帧, 随后就被回收了, 所以是不可能达到递归栈的深度的限制的。

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

              

Python递归优化方法的更多相关文章

  1. 用Python递归解决阿拉伯数字转为中文财务数字格式的问题(2)--打开思路的一种方法

    几天前自己写了个将阿拉伯数字转为中文财务数字的程序.用的递归,不幸的是它是树形递归. 虽然实际过程中不太可能出现金额数字大到让Python递归栈溢出,但是始终是一块心病,这玩意终究在理论上是受限制的. ...

  2. python内置方法

    1. 简介 本指南归纳于我的几个月的博客,主题是 魔法方法 . 什么是魔法方法呢?它们在面向对象的Python的处处皆是.它们是一些可以让你对类添加"魔法"的特殊方法. 它们经常是 ...

  3. python之魔法方法介绍

    1.1. 简介 什么是魔法方法呢?它们在面向对象的Python的处处皆是.它们是一些可以让你对类添加“魔法”的特殊方法. 它们经常是两个下划线包围来命名的(比如 __init__ , __lt__ ) ...

  4. python递归三战:Sierpinski Triangle、Tower of Hanoi、Maze Exploring

    本文已做成视频教程投稿b站(视频版相对文本版有一些改进),点击观看视频教程 本文主要通过三个实例来帮助大家理解递归(其展示动画已上传B站): 谢尔宾斯基三角形(Sierpinski Triangle) ...

  5. python性能优化

      注意:本文除非特殊指明,”python“都是代表CPython,即C语言实现的标准python,且本文所讨论的是版本为2.7的CPython. python为什么性能差: 当我们提到一门编程语言的 ...

  6. Python内存优化

    实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章<Python性能优化>介绍Python性能优化的一些方法.而本文,关注的是Python的内存优化,一般说 ...

  7. 【转】Python的神奇方法指南

    [转]Python的神奇方法指南 有关Python内编写类的各种技巧和方法(构建和初始化.重载操作符.类描述.属性访问控制.自定义序列.反射机制.可调用对象.上下文管理.构建描述符对象.Picklin ...

  8. redmine在linux上的mysql性能优化方法与问题排查方案

    iredmine的linux服务器mysql性能优化方法与问题排查方案     问题定位:   客户端工具: 1. 浏览器inspect-tool的network timing工具分析   2. 浏览 ...

  9. Python内存优化:Profile,slots,compact dict

    实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章<Python性能优化>介绍Python性能优化的一些方法.而本文,关注的是Python的内存优化,一般说 ...

随机推荐

  1. duilib进阶教程 -- 图片和文字的位置调整 (5)

    已经有8个晚上没写教程啦,因为之后遇到了一些问题,主要是TreeView控件的问题,这个问题搞了几个晚上,然后还需要调试代码才能知道它的用法,虽然能够调试出来,但毕竟没什么含金量,只是重复劳动而已,相 ...

  2. [IR] Bigtable: A Distributed Storage System for Semi-Structured Data

    良心博文: http://blog.csdn.net/opennaive/article/details/7532589 这里只是基础简述 众人说: 链接:http://blog.csdn.net/o ...

  3. 51 IP核查询

    康芯的IP核 Oregano systems 公司的MC8051 IP CoreSynthesizeable VHDL Microcontroller IP-Core User Guide这个里面51 ...

  4. OpenGL——旋转的六边形(动画)

    代码: #include<iostream> #include <math.h> #include<Windows.h> #include <GL/glut. ...

  5. Linux设备驱动剖析之IIC(三)

    下面以eeprom用户程序调用ioctl函数的写操作为例追踪IIC子系统的调用过程.eeprom的用户测试是大部分开发板都自带的.看写一个字节数据的eeprom_write_byte函数的定义: in ...

  6. lsass 病毒手动清除方法

    病毒症状进程里面有2个lsass.exe进程,一个是system的,一个是当前用户名的(该进程为病毒).双击D:盘打不开,只能通过右击选择打开来打开.用kaspersky扫描可以扫描出来,并且可以杀掉 ...

  7. D - Football (aka Soccer)

    Football the most popular sport in the world (americans insist to call it "Soccer", but we ...

  8. vscode 的 vue模板

    12 { "Print to console": { "prefix": "vue", "body": [ " ...

  9. Jwt 中 token应该存储到哪里?

    关于 token 的存储问题 JWT: csrf 攻击无法获取第三方的 cookie,而是直接使用 cookie进行查询的时候会自动携带 cookie. xss攻击通过代码注入可以获取 cookie. ...

  10. Flask web开发之路二

    今天创建第一个flask项目,主app文件代码如下: # 从flask这个框架导入Flask这个类 from flask import Flask #初始化一个Flask对象 # Flasks() # ...