如何在Python中实现尾递归优化

编程之美:Python 专栏收录该内容
51 篇文章 2 订阅

任何关于算法、编程、AI行业知识或博客内容的问题,可以随时扫码关注公众号「图灵的猫」,加入”学习小组“,沙雕博主在线答疑~此外,公众号内还有更多AI、算法、编程和大数据知识分享,以及免费的SSR节点和学习资料。其他平台(知乎/B站)也是同名「图灵的猫」,不要迷路哦~

 

一般递归

 
 
 
 
 
1
2
3
4
5
6
def normal_recursion ( n ) :
     if n == 1 :
         return 1
     else :
         return n + normal_recursion ( n - 1 )
 

执行:

 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
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
 

可以看到, 一般递归, 每一级递归都需要调用函数, 会创建新的栈,

随着递归深度的增加, 创建的栈越来越多, 造成爆栈

 

尾递归

尾递归基于函数的尾调用, 每一级调用直接返回函数的返回值更新调用栈,而不用创建新的调用栈, 类似迭代的实现, 时间和空间上均优化了一般递归!

 
 
 
 
 
1
2
3
4
5
6
def tail_recursion ( n , total = 0 ) :
     if n == 0 :
         return total
     else :
         return tail_recursion ( n - 1 , total + n )
 

执行:

 
 
 
 
 
1
2
3
4
5
6
7
8
tail_recursion ( 5 )
tail_recursion ( 4 , 5 )
tail_recursion ( 3 , 9 )
tail_recursion ( 2 , 12 )
tail_recursion ( 1 , 14 )
tail_recursion ( 0 , 15 )
15
 

可以看到, 每一级递归的函数调用变成”线性”的形式.

深入理解尾递归

呃, 所以呢? 是不是感觉还不够过瘾… 谁说尾递归调用就不用创建新的栈呢?
还是让我们去底层一探究竟吧

 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 不支持尾递归,递归深度超过1000时会报错

 
 
 
 
 
1
2
RuntimeError : maximum recursion depth exceeded
 

 

一个牛人想出的解决办法

实现一个 tail_call_optimized 装饰器

 

 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/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 ( )
         # 为什么是grandparent, 函数默认的第一层递归是父调用,
         # 对于尾递归, 不希望产生新的函数调用(即:祖父调用),
         # 所以这里抛出异常, 拿到参数, 退出被修饰函数的递归调用栈!(后面有动图分析)
         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 )
 

为了更清晰的展示开启尾递归优化前、后调用栈的变化和tail_call_optimized装饰器抛异常退出递归调用栈的作用, 我这里利用pudb调试工具做了动图

开启尾递归优化前的调用栈

 

开启尾递归优化后(tail_call_optimized装饰器)的调用栈

 

通过pudb右边栏的stack, 可以很清晰的看到调用栈的变化.

因为尾递归没有调用栈的嵌套, 所以Python也不会报RuntimeError: maximum recursion depth exceeded错误了!

这里解释一下 sys._getframe() 函数:

 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 # 调用者的帧
 

更多关于sys._getframe请看Frame Hacks

 

  • 3
    点赞
  • 0
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值