为什么 Python 不支持尾递归优化

原作者:Guido
原文地址


我最近在 Python 之历史博客中发布了一篇关于 Python “函数式” 起源 的文章。“不支持尾递归消除(TRE)”这一句话带来了类似 “很遗憾 Python 不会这么做” 的评论,以及尝试 “证明” TRE 可以容易地添加到 Python 中的博客。那么让我捍卫自己的立场:我不希望 Python 有 TRE。一个简短的答案就是 TRE 一点也不 Pythonic。 下面是长答案:

首先,正如一位评论者所言,TRE 与优雅的栈跟踪信息是不相容的:尾部递归被消除后,若出现错误,没有任何堆栈框架可用于打印回溯。这会混淆那些无意中使用递归的用户(递归在打印的栈跟踪中不明显),并且使调试变得困难。提供一个禁用 TRE 的选项对我来说是错误的:Python 的默认值应该总是对调试最有帮助。这也让我能提出接下的事情。

其次,TRE 仅仅是一种优化,每个 Python 实现都可以选择实现或不实现的想法是错误的。一旦存在尾递归消除,开发人员将开始编写依赖于它的代码,并且他们的代码将无法在没有提供 TRE 的实现上运行:典型的 Python 实现允许 1000 次递归,这对于非递归或用于递归遍历的代码是足够的,例如,一个经典的解析树,但不足以用于递归遍历一个大的列表。

第三,我不相信递归是所有编程的基础。这是某些计算机科学家的基本信念,特别是那些热爱 Scheme 并喜欢以 cons 单元和递归作为编程教程开头的人。但对我而言,将递归看作是所有其他事物的基础只是一个很好的理论方法,而不是日常的工具。

出于实际的目的,对于刚开始探索编程世界来说,Python 风格的列表(它们是灵活的数组,而不是链表),以及一般的序列,比递归更有用。对于有经验的 Python 程序员来说,它们也是最重要的工具。使用链表来表示序列显然是不够 Pythonic,同时在大多数情况下效率非常低。 Python 的大部分库都是用序列和迭代器作为基本构建块(当然还有词典)编写的,而不是链表,所以不使用列表或序列你将无法享受许多预定义的实用功能。

最后,让我们看看如何实现尾递归消除。第一个值得注意的是你不能在编译时这么做。我已经看到至少有一个博客利用字节码在 RETURN 操作之前替换 CALL操作,改为跳转到函数体的顶部。这可能是一个很好的演示,但不幸的是,Python 的编译器不能可靠地确定任何特定的调用是否实际上引用了当前函数,即使它看起来具有相同的名称。考虑这个简单的例子:

def f(x):
    if x > 0:
        return f(x-1)
    return 0

看起来你可以用这样的东西替换函数体:

if x >  0:
    x = x-1
    # <跳转到顶部>
return 0

这似乎很简单,但现在添加:

g = f
def f(x):
    return x
g(5)

调用 g(5) 时用到了前一个f,但“递归”调用不再递归!在运行时,名字 f 被再绑定到后面的非递归定义的版本,所以返回的值是 4,而不是 0。虽然我同意这个例子是不好的风格,但它是 Python 明确定义的语义,具有大量合法用途。一个编译器如果乐观地认为 f 的定义保持不变,会因这种替换在现实世界的代码中导致令人发指的错误。

另一篇博客展示了可用于使用神奇异常或返回值实现尾递归的装饰器。这些可以用普通的 Python 编写(尽管这篇文章展示了一个优化的 Cython 版本,声称它“速度只慢了10%”,尽管它似乎不是线程安全的)。如果这让你想去使用,我不会试图阻止你,但我强烈反对在核心发行版中包含这样的内容:使用这样的装饰器有许多警告,因为它必须假定任何递归调用(在装饰函数中)是尾递归的并且可以被消除。在经验不足的用户手中,这很容易导致灾难。例如,阶乘的通用递归定义不是尾递归的:

def fact(n):
    if n > 1:
        return n * fact(n-1)
return 1

还有有很多函数包含一个尾递归调用和另一个不是尾递归的递归调用,装饰器不处理这种情况。这些装饰器无法处理的另一个微妙之处是在 try 块内部进行尾递归调用:这些调用块可能看起来像可以被删除,但它们不能,因为 TRE 可以删除由语言保证的异常处理。由于所有这些原因,我认为这个方法是糟糕的,至少对于一般的用户来说。

但是,如果有人决定将 TRE 添加到 CPython,他们可以大致如下修改编译器。首先,确定“安全”尾递归呼叫站点。这就像是一个 CALL 操作码,紧跟着一个 RETURN 操作码,并且完全在任何 try 块之外。(注意:我忽略了不同的 CALL_ *操作码,这应该很容易使用相同的方法处理。)接下来,用一个 CALL_RETURN 操作码替换每个这样的 CALL - RETURN 操作码对。编译器不需要尝试检查被调用函数的名称是否与当前函数相同:只需通过节省解码第二个操作码的时间,新操作码就可以节省所有 CALL + RETURN 组合的成本。如果在运行时确定此特定调用不适用于 TRE,则执行 CALLRETURN 操作码之后的通常操作。(我想你可以添加一些对调用位点索引过缓存机制来加速运行时间的决定。)

在确定可以优化时,可以设置几种优化级别。如果被调用的对象是已经在当前栈帧中运行的函数,那么最不积极的 vanilla 方法将只优化调用。我们现在要做的就是清除当前堆栈框架中的局部(以及其他隐藏状态,例如活动循环),设置赋值栈中的参数并跳转到函数顶部。 (微妙地,根据定义,新的参数在当前的堆栈中。可能只是需要先复制它们,更微妙的是关键字参数,可变长度参数列表和默认参数值的存在。虽然这只是一个简单的编程问题。)

一个更激进的版本也将识别出一种方法是尾递归的情况(即被调用的对象是一个方法,其底层函数与当前栈帧中的函数相同)。这只需要更多的编程;CPython 解释器代码(ceval.c)已经对方法调用进行了优化。(我不知道这将是多么有用:我期望尾部递归风格受到喜欢使用函数式编程风格的程序员的欢迎,并且可能不会使用太多的类。)

从理论上讲,只要新调用所需的局部变量的数量可以在当前堆栈框架对象中调节,就可以优化所有被调用的对象是用 Python 编写的函数或方法的情况。(CPython中的Frame对象被分配到堆上,并且具有基于本地所需空间的变量分配大小;已经有用于重用框架对象的机器)。这将优化相互的尾递归函数,否则将不会是优化。唉,它也会在大多数情况下禁用堆栈跟踪,所以它可能不是一个好主意。

一个更好的变体是像以前一样创建 Python 级别的堆栈对象,但重用 C 堆栈框架。这将创建一个近似无栈的 Python,尽管递归内置的函数或方法,仍然可以很容易地耗尽 C 的堆栈。

当然,这些都不能解决我的前三个问题。重写你的函数来使用循环是否真的是一件大事?(毕竟 TRE 只处理可以很容易被循环替换的递归)

“Python” 尾递归
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,793评论 0 38
  • 〇、前言 本文共108张图,流量党请慎重! 历时1个半月,我把自己学习Python基础知识的框架详细梳理了一遍。 ...
    Raxxie阅读 18,954评论 17 410
  • 今天突然意识到,出国两年以来,我竟只买过一本中文书——汉娜·阿伦特选编的本雅明文选《启迪》,还是因为我之前的那一本...
    燕仰阅读 601评论 2 13
  • 最近很烦躁,不管是工作还是生活都是一团糟,我已经记不清多久没有正常的和文先生一起吃过一次饭了,不断的忙碌不断的忙碌...
    三文鱼平平阅读 115评论 0 0
  • 布局优化:布局优化的思想就是减少布局文件的层级。 标签:将一个指定的布局文件加载到当前的布局文件。 标签只支持以 ...
    L_Xian阅读 274评论 0 0