专栏名称: Python程序员
最专业的Python社区,有每日推送,免费电子书,真人辅导,资源下载,各类工具。我已委托“维权骑士”(rightknights.com)为我的文章进行维权行动
目录
相关文章推荐
Python爱好者社区  ·  商汤科技,爆了。。。 ·  4 天前  
Python爱好者社区  ·  面向开发者的 NVIDIA ... ·  4 天前  
Python爱好者社区  ·  王者归来!《一书解决几乎所有机器学习问题》PDF ·  1 周前  
Python爱好者社区  ·  42岁,讲师,因为评职称郁郁寡欢,吃了半年的 ... ·  1 周前  
Python爱好者社区  ·  商汤科技,彻底爆了 ·  1 周前  
51好读  ›  专栏  ›  Python程序员

尾递归 —— 写给命令式编程程序员

Python程序员  · 公众号  · Python  · 2017-07-12 08:10

正文

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

      在Recurse Center期间,我通读了《计算机程序的构造和解释》。这是一本出版于1985年的MIT的编程入门教材,用LISP的一种方言Scheme语言来教授编程。Scheme是函数式语言,我也一直喜欢学习新的函数式编程概念。

      本文目的是向没有函数式语言经验或概念的程序员解释“尾递归”。

      在解释尾递归之前,我们先看看命令式语言Python中的递归。

递归的一个问题

      上面这段代码定义了一个返回数字n的阶乘的函数:factorial(n) = n * n - 1 * ... * 2 * 1。如果n = 4, 可以获得期望的结果是4 * 3 * 2 * 1 = 24。

      当我们运行一个递归函数时,在运行背后到底发生了什么?任一函数被调用,就会往进程堆栈中加入一个包含与该函数有关的数据的栈帧,通过Python中的inspect模块,我们可以观察到发生了什么:

      运行这个脚本得到:

      我们可以看到,当inspect.stack()第一次被调用时,一个栈帧加到了堆栈中。再次调用,堆栈中就有两个栈帧了。

      栈帧消耗内存,而一个Python进程只能分配到有限的内存。如果堆栈包含太多的栈帧,进程会耗尽所分配的内存,或者堆栈可能扩展到没有分配给该进程的内存空间而导致堆栈溢出。为了防止出现这样的问题,解释器会设置一个最大递归数的限制,可以通过调用sys.getrecursionlimit()来查看这个限制。在我的计算机上,这个限制是1000(注)。

      每一次调用factorial(),一个新的栈帧就会加到堆栈中。如果加入了太多的栈帧,就会超过这个最大递归数的限制,解释器就会抛出如下异常:

      由于栈帧产生系统开销,所以在命令式语言中,递归是内存密集型的操作。相比下面的使用迭代方式获取阶乘的函数:

      这个函数只产生了一个栈帧,比递归方式能更轻易处理大于n十倍或者更大的数值。

迭代递归

      思考当解释器执行factorial(4)时发生了什么:

      我们能看到创建了一系列延迟的操作,直到遇到n = 1时才会计算出最终结果。这是因为解释器为了后面的执行必须要记住并跟踪前面的操作。

      如果我们重新定义factorial函数为:

并重新思考解释器做了什么:

我们看到一系列扁平的factorial()调用,状态保存在变量total中,而不是由解释器来保存。

尾递归

      在“尾递归”语言中,以factorial_new这种方式定义的递归过程可以被解释器解释为迭代,从而不会带来递归的缺点。用这种优雅的递归过程,能使你获得和迭代一样的性能优势。解释器知道不需要在堆栈上创建更多的栈帧,从而抛弃创建栈帧的方式。

      不幸的是,Python不是“尾递归”语言,所以factorial_new(1000, 1)仍然会抛出RuntimeError: maximum recursion depth exceeded。

      更多信息,推荐参考《计算机程序的构造和解释》中的1.2.1小节。

      注:在Python中可以调用sys.setrecursionlimit()来设置最大递归数,但通常不建议这么做。递归调用太多的函数应该使用迭代的方式来重写。


英文原文:https://jamesroutley.co.uk/tech/2017/06/04/tail-recursion.html
译者:scala