详解Python如何实现尾递归优化
作者:misakar 时间:2023-11-13 04:20:06
尾递归是一种特殊的递归形式,它在递归调用时不会产生新的栈帧,从而避免了栈溢出的问题。Python并没有对尾递归进行优化,但我们可以通过一些技巧来实现递归优化。本文将详细介绍Python如何实现尾递归优化,并提供两个示例来说明它的用法。
什么是尾递归
在介绍如何实现尾递归优化之前,我们先来了解一下什么是尾递归。
递归是指递归函数在调用自身之后,不再有其他操作,直接返回结果。这种形式的递归可以被优化为迭代形式,从而避免了栈溢出的问题。
例如,下面是一个阶乘函数的递归实现:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
这个函数不是尾递归,因为在递归调用之后还有其他操作(乘法)。如果我们将其改写为尾递归形式,可以得到下代码:
def factorial, acc=1):
if n == 0:
return acc
else:
return factorial(n-1, acc*n)
在这个函数中,我们引入了一个额外的参数acc,用于保存中间结果。在递归调用时,我们将中结果乘以当前的n,并将结果传递给下一次递归调用。当递归到n=0时,我们直接返回中间结果``,从而避免了栈溢出的问题。
如何实现尾递归优化
Python并没有对尾递归进行优化,但我们可以通过一些技巧来实现尾递归优化。具体来,我们可以使用循环、函数参数等方式来避免递归调用产生新的栈帧。
使用循环
使用循环是一种常见的实现尾递归优化的方式。例如,下面是一个使用循环实现阶乘函数的代码:
def factorial(n):
acc = 1
while n > 0:
acc *= n
n -= 1
return acc
在这个函数中,我们使用循环来计算阶乘,避免了递归调用产生新的栈帧。
使用函数参数
使用函数参数也是一种实现尾递归优化的方式。例如,下面是一个使用函数参数实现阶乘函数的代码:
def factorial(n, acc=1):
if n == 0:
return acc
else:
return factorial(n-1, acc*n)
在这个函数中,我们引入了一个额外的参数acc,用于保存中间结果。在递归调用时,我们将中间结果乘以当前的参数n,并将结果传递给下次递归调用。当递归到n=0时,我们直接返回中间结果acc从而避免了栈溢出的问题。
示例1:使用循环实现斐波那契数列
下面是一个使用循环实现斐波那契数列的示例:
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
a, b = 0, 1
for i in range(n-1):
a, b = b, a+b
return b
在这个函数中,我们使用循环来计算斐波那契数列的第n项,避免了递归调用产生新的栈帧。
示例2:使用函数参数实现尾递归优化
下面是一个使用函数参数实现尾递归优化的阶乘函数的示例:
def factorial(n, acc=1):
if n == 0:
return acc
else:
return factorial(n-1, acc*n)
在这个函数中,我们引入了一个额外的参数acc,用于保存中间结果。在递归调用时,我们将中间结果乘以当前的参数n,并将结果传递给下一次递调用。当递归到=0时,我们直接返回中间结果acc,从而避免了栈溢出的问题。
一般递归与尾递归
一般递归
def normal_recursion(n):
if n == 1:
return 1
else:
return n + normal_recursion(n-1)
执行:
normal_recursion(5)
5 + normal_recursion(4)
5 + 4 + normal_recursion(3)
5 + 4 + 3 + normal_recursion(2)
5 + 4 + 3 + 2 + normal_recursion(1)
5 + 4 + 3 + 3
5 + 4 + 6
5 + 10
15
可以看到, 一般递归, 每一级递归都产生了新的局部变量, 必须创建新的调用栈, 随着递归深度的增加, 创建的栈越来越多, 造成爆栈?
尾递归
尾递归基于函数的尾调用, 每一级调用直接返回递归函数更新调用栈, 没有新局部变量的产生, 类似迭代的实现:
def tail_recursion(n, total=0):
if n == 0:
return total
else:
return tail_recursion(n-1, total+n)
执行:
tail_recursion(5, 0)
tail_recursion(4, 5)
tail_recursion(3, 9)
tail_recursion(2, 12)
tail_recursion(1, 14)
tail_recursion(0, 15)
15
可以看到, 尾递归每一级递归函数的调用变成"线性"的形式. 这时, 我们可以思考, 虽然尾递归调用也会创建新的栈, 但是我们可以优化使得尾递归的每一级调用共用一个栈!, 如此便可解决爆栈和递归深度限制的问题!
C中尾递归的优化
gcc使用-O2
参数开启尾递归优化:
int tail_recursion(int n, int total) {
if (n == 0) {
return total;
}
else {
return tail_recursion(n-1, total+n);
}
}
int main(void) {
int total = 0, n = 4;
tail_recursion(n, total);
return 0;
}
反汇编
$ gcc -S tail_recursion.c -o normal_recursion.S
$ gcc -S -O2 tail_recursion.c -o tail_recursion.S gcc开启尾递归优化
对比反汇编代码如下(AT&T语法, 左图为优化后)
可以看到, 开启尾递归优化前, 使用call调用函数, 创建了新的调用栈(LBB0_3); 而开启尾递归优化后, 就没有新的调用栈生成了, 而是直接pop bp指向的_tail_recursion函数的地址(pushq %rbp)然后返回, 仍旧用的是同一个调用栈!
Python开启尾递归优化
cpython本身不支持尾递归优化, 但是一个牛人想出的解决办法:实现一个 tail_call_optimized 装饰器
#!/usr/bin/env python2.4
# This program shows off a python decorator(
# which implements tail call optimization. It
# does this by throwing an exception if it is
# it's own grandparent, and catching such
# exceptions to recall the stack.
import sys
class TailRecurseException:
def __init__(self, args, kwargs):
self.args = args
self.kwargs = kwargs
def tail_call_optimized(g):
"""
This function decorates a function with tail call
optimization. It does this by throwing an exception
if it is it's own grandparent, and catching such
exceptions to fake the tail call optimization.
This function fails if the decorated
function recurses in a non-tail context.
"""
def func(*args, **kwargs):
f = sys._getframe()
if f.f_back and f.f_back.f_back \
and f.f_back.f_back.f_code == f.f_code:
# 抛出异常
raise TailRecurseException(args, kwargs)
else:
while 1:
try:
return g(*args, **kwargs)
except TailRecurseException, e:
args = e.args
kwargs = e.kwargs
func.__doc__ = g.__doc__
return func
@tail_call_optimized
def factorial(n, acc=1):
"calculate a factorial"
if n == 0:
return acc
return factorial(n-1, n*acc)
print factorial(10000)
这里解释一下sys._getframe()
函数:
sys._getframe([depth]):Return a frame object from the call stack.
If optional integer depth is given, return the frame object that many calls below the top of the stack.
If that is deeper than the call stack, ValueEfror is raised. The default for depth is zero,
returning the frame at the top of the call stack.
即返回depth深度调用的栈帧对象.
import sys
def get_cur_info():
print sys._getframe().f_code.co_filename # 当前文件名
print sys._getframe().f_code.co_name # 当前函数名
print sys._getframe().f_lineno # 当前行号
print sys._getframe().f_back # 调用者的帧
说一下tail_call_optimized实现尾递归优化的原理: 当递归函数被该装饰器修饰后, 递归调用在装饰器while循环内部进行, 每当产生新的递归调用栈帧时: f.f_back.f_back.f_code == f.f_code:
, 就捕获当前尾调用函数的参数, 并抛出异常, 从而销毁递归栈并使用捕获的参数手动调用递归函数. 所以递归的过程中始终只存在一个栈帧对象, 达到优化的目的.
为了更清晰的展示开启尾递归优化前、后调用栈的变化和tail_call_optimized装饰器抛异常退出递归调用栈的作用, 我这里利用pudb调试工具做了动图:
开启尾递归优化前的调用
开启尾递归优化后(tail_call_optimized装饰器)的调用
通过pudb右边栏的stack, 可以很清晰的看到调用栈的变化.
因为实现了尾递归优化, 所以factorial(10000)都不害怕递归深度限制报错啦!
来源:https://segmentfault.com/a/1190000007641519