python里大多数人忽略的try/finally陷阱,为什么不推荐在finally里写return
当你处理外部资源(如数据库或临时文件)时,通常需要在代码完成后,执行某些清理操作,比如file.close(),connect.close()。Python提供了两种常用的操作方法:上下文管理器 (with语法) 和 try/finally 代码块。两者都是有效的方案,但上下文管理器通常被赞誉为更具Python风格的代码。尽管如此,try/finally 块还是被广泛使用(可能是其他语言的惯性带过来的吧)。try/finally 简单易用且在某些情况下非常合适,但它隐藏着陷阱。 try/finally 模式的变体存在于大多数语言中,其运行方式和你想象的是一样的。简单的例子: def walk_path(): try: print("第一步") finally: print("第二步") #执行时打印内容: #第一步 #第二步
代码进入第一个代码块并执行语句,最后再进入finally块。finally块总是会执行,无论第一个块是成功还是失败。如果执行过程中抛出了错误(如下所示),错误会中断正常流程,解释器立即停止执行 try 块,并直接跳到 finally 块。def walk_path(): try: raise Exception("发出异常") finally: print("第二步")
如果你选择使用 except 块来处理错误,错误会在进入 finally 块之前被 except 块捕获并处理。即便在 except 内部发生了错误,解释器依然会在抛出新错误之前执行 finally 块。这要从函数返回过程说起了。函数可以在 try 块内部返回一个值。当解释器遇到 return 语句时,它准备将此值传回调用者。然而,它如果还有 finally 块:它会暂停返回过程,先执行 finally 块,然后再返回 try 块中准备好的值。然而如果遇到了finally块里也有return,这时候你能想象会发生什么事情吗?def walk_path(): try: return "到达目的地" finally: return "返回家中" # 这时候会返回“返回家中”
当这种情况发生时,finally 块中的 return 会胜出,而来自 try 块的返回值会被有效地忽略。这样的行为同样适用于发生异常时,如果try块里返回的异常,但是finally有return时,那么异常并不会抛出给上级函数在try块和finally块都存在return时,代码虽然都会执行,但是try块的return永远都不会return,因为return的结果被finally覆盖了。这样导致最大的问题就是:当代码报异常时,异常其实也算是一种返回值,finally里的return会把这个异常给吞了,变成finally里的返回值任何允许你编写会产生意外结果的“死代码”的语言都是有问题的。Python 的开发者们 意识到了这个问题,并在 PEP601提议中提议禁止在 finally 块中使用 return/break/continue(break/continue也和return有一样的效果,会篡改返回值)。但是该提议被否决了阅读PEP中的参考文献后,我认为大多数语言都实现了这种结构,但它们的风格指南和/或代码检查工具却不接受它。
我支持将此内容添加到PEP 8中的提案(如果尚未添加的话)。
我注意到这些玩具示例有点误导性——真正有用的功能是在 finally 代码块内使用条件返回(或 break 等)。
-Guido 2019 PEP601
Guido 的理由是,在某些合法场景中,用户可能需要完全控制 finally 块中的异常处理,并希望覆盖异常的抛出。阻止这种行为会限制高级用户。开发者们掌握了证据。他们分析了 PyPI 排名前 8000 的软件包,发现:在 finally 中使用 return 的绝大多数情况都是错误的,并且引入了意外吞掉异常的 bug。 - PEP765于是该提案被通过了,从 Python 3.14 开始,在finally中使用return、break、continue时会报一个SyntaxWarning的异常。在python3.14以前,由于finally里可以使用return、continue、break等操作,会导致原本应该正常抛出的异常,被finally的返回值顶替 。代码语言设计的逻辑上没问题,不代表不需要人为干预。finally块的逻辑上不存在任何问题,只是人们太容易忽略这个问题且出错了,所以python开发者们通过分析现有项目的使用情况就大胆禁用了finally里return的语法。而反观其他语言如java,但现在都只有人为的规范,并没有强制限制这个特性。这可能就是python越来越好用的原因吧。部分翻译自substack文章,原文放原文链接里了