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

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

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

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中的尾递归调用有一段神奇的代码:

import sys

class TailCallException:

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) #突破了调用栈的深度限制

以上的代码是怎样的工作的呢?

理解它需要对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 ...), 该异常会从当前栈帧开始向旧的执行栈帧传递, 直到有一个旧的栈帧捕获这个异常, 而该栈帧之后(比它更新的栈帧)的栈帧就被回收了。

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

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

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

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

由于我们是第一次调用, 所以”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显然不满足。

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

由于是函数调用, 所以虚拟机会创建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这个函数对象。

依然是函数调用的一套, 创建执行栈帧, 我们称之为frame_wrapper2, 注意: 执行栈帧是动态生成的, 虽然对应的是同样函数对象(_wrapper), 但依然是不同的栈帧对象, 所以称之为frame_wrapper2。 今后进入frame_wrapper2执行, 注意此时的虚拟机的运行时栈帧的结构按从旧到新为:

旧的虚拟机栈帧、frame_wrapper、frame_real_fib、frame_wrapper2(当前执行栈帧)

进入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递归详解_打破递归栈的深度限制: 解析一种Python尾递归优化的方法相关推荐

  1. python递归详解_Python理解递归的方法总结

    递归 一个函数在执行过程中一次或多次调用其本身便是递归,就像是俄罗斯套娃一样,一个娃娃里包含另一个娃娃. 递归其实是程序设计语言学习过程中很快就会接触到的东西,但有关递归的理解可能还会有一些遗漏,下面 ...

  2. python argv 详解_对python中的argv和argc使用详解

    主要问题 为什么argv中第一个,即index=0的内容就是文件名? python中argc是用什么实现的? 概念解释 argc:argument counter,命令行参数个数 argv:argum ...

  3. python学习详解_深入解析Python小白学习【操作列表】

    1.遍历列表 需要对列表中的每个元素都执行相同的操作时,可使用for 循环: magicians = ['alice','david','carolina'] for magician in magi ...

  4. python 迭代详解_详解python中的迭代

    如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration). 在Python中,迭代是通过for ... in来完成的,而很多语 ...

  5. python语法详解_关于python:NLTK中解析的英语语法

    是否有可以立即使用并可以在NLTK中使用的即用型英语语法? 我搜索了使用NLTK进行解析的示例,但似乎我必须在解析句子之前手动指定语法. 非常感谢! 您可以看一下pyStatParser,这是一个简单 ...

  6. python解释器详解_浅析Python解释器的设计(一)

    一些铺垫(扯淡) 历史上,在Python 2.4以及之前的版本,py代码的执行,也就是从源码到bytecode分为两步: 解析py源码成为分析树 (Parser/pgen.c)基于分析树优化缩减byt ...

  7. python递归详解_python基于递归解决背包问题详解

    递归是个好东西,任何具有递归性质的问题通过函数递归调用会变得很简单.一个很复杂的问题,几行代码就能搞定. 最简单的递归问题:现有重量为weight的包,有若干重量分别为W1,W2.....Wn的物品, ...

  8. python 快速排序 详解_数据结构与算法:快速排序(原理讲解+python实现)

    快速排序 快速排序是一种基于分治法(Divide and Conquer)的排序算法 它之所以称为快速排序是因为它的平均时间复杂度为O(nlogn),最坏情况下是O(n2) 但是这样的情况不常见 一般 ...

  9. 100行的python作品详解_漫画喵的100行Python代码逆袭

    小喵的唠叨话:这次的博客,讲的是使用python编写一个爬虫工具.为什么要写这个爬虫呢?原因是小喵在看完<极黑的布伦希尔特>这个动画之后,又想看看漫画,结果发现各大APP都没有资源,最终好 ...

最新文章

  1. 数学知识--Methods for Non-Linear Least Squares Problems(第一章)
  2. AI技术如何帮助研究人员重现历史的气味?
  3. UVALive - 3231 Fair Share(最大流+二分)
  4. hotspot线程模型_Linux上的HotSpot GC线程CPU占用空间
  5. PHP 社区拒绝在俄乌冲突中“站队”
  6. php json encode中文乱码,php json_encode中文乱码如何解决
  7. 酱茄企业官网多端开源小程序源码 v1.0.0
  8. 使用Google Font API
  9. Java实现将二进制文件显示为图片(SU中的ximage)
  10. swagger里面显示的示例参数格式错误
  11. java 购物系统代码_java购物系统源代码
  12. java-nio网络编程
  13. Linux TCP之sack(二)
  14. oracle的并行原理
  15. 历届蓝桥杯Scratch编程国赛 初级 中级 青少年编程比赛国赛真题解析【持续更新 已更新至27题】
  16. Kubernetes Pod日志太大导致磁盘空间的问题
  17. java 电子时钟_java多线程编程制作电子时钟
  18. web网站服务器宕机应急,web服务器的宕机诊断方法
  19. Mathcad的使用与设计
  20. Window截图方法

热门文章

  1. 根据下拉框生成控件列表
  2. var和dynamic的应用 var、动态类型 dynamic 深入浅析C#中的var和dynamic ----demo
  3. 回滚 - 每天5分钟玩转 Docker 容器技术(141)
  4. 在(CListView)列表视图中添加右键菜单的方法
  5. html5 Web Workers
  6. C#:设置当前线程的区域性
  7. 认识计算机系统反思,《认识计算机系统》教学反思
  8. java resultset wasnull_Java Spring – RowMapper ResultSet – 整数/空值
  9. crc16modbus查表法_查表法计算CRC16校验值
  10. java映射文件是哪一种xml_java解析xml的几种方式哪种最好?