在 Python 开发中,try/finally 是常见的资源清理模式,但其中隐藏的陷阱却常被忽略。今天,云栈社区 就来剖析这个让无数人踩过的坑。
先看一个例子:

运行结果令人意外:它会显示 finally返回,而不是 try 块中的返回值。

场景:如何操作关闭文件资源/临时文件/数据库等?
当你处理外部资源(如数据库或临时文件)时,通常需要在代码完成后,执行某些清理操作,比如 file.close(),connect.close()。
Python 提供了两种常用的操作方法:上下文管理器 (with 语法) 和 try/finally 代码块。两者都是有效的方案,但上下文管理器通常被赞誉为更具 Python 风格的代码。
尽管如此,try/finally 块还是被广泛使用(可能是其他语言的惯性带过来的吧)。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 块。
也就是 finally 块无论如何都会被运行。
finally 的陷阱
这要从函数返回过程说起了。函数可以在 try 块内部返回一个值。当解释器遇到 return 语句时,它准备将此值传回调用者。然而,如果它后面还有 finally 块:它会暂停返回过程,先执行 finally 块,然后再返回 try 块中准备好的值。
然而如果 finally 块里也有 return,这时候你能想象会发生什么事情吗?
def walk_path():
try:
return "到达目的地"
finally:
return "返回家中"
# 这时候会返回“返回家中”
当这种情况发生时,finally 块中的 return 会胜出,而来自 try 块的返回值会被有效地忽略。
这样的行为同样适用于发生异常时,如果 try 块里返回的异常,但是 finally 有 return 时,那么异常并不会抛出给上级函数。
finally 的问题:被吞掉异常
在 try 块和 finally 块都存在 return 时,代码虽然都会执行,但是 try 块的 return 永远都不会 return,因为 return 的结果被 finally 覆盖了。
这样导致最大的问题就是:当代码报异常时,异常其实也算是一种返回值,finally 里的 return 会把这个异常给吞了,变成 finally 里的返回值。
任何允许你编写会产生意外结果的“死代码”的语言都是有问题的。Python 的开发者们意识到了这个问题,并在 PEP 601 提议中提议禁止在 finally 块中使用 return/break/continue(break/continue 也和 return 有一样的效果,会篡改返回值)。但是该提议被否决了。
阅读 PEP 中的参考文献后,我认为大多数语言都实现了这种结构,但它们的风格指南和/或代码检查工具却不接受它。
我支持将此内容添加到 PEP 8 中的提案(如果尚未添加的话)。
我注意到这些玩具示例有点误导性——真正有用的功能是在 finally 代码块内使用条件返回(或 break 等)。
-Guido 2019 PEP601
Guido 的理由是,在某些合法场景中,用户可能需要完全控制 finally 块中的异常处理,并希望覆盖异常的抛出。阻止这种行为会限制高级用户。
但是 2024 年这个问题又被提出来了:
开发者们掌握了证据。他们分析了 PyPI 排名前 8000 的软件包,发现:
在 finally 中使用 return 的绝大多数情况都是错误的,并且引入了意外吞掉异常的 bug。 - PEP765
于是该提案被通过了,从 Python 3.14 开始,在 finally 中使用 return、break、continue 时会报一个 SyntaxWarning 的异常。
在 Python 3.14 以前,由于 finally 里可以使用 return、continue、break 等操作,会导致原本应该正常抛出的异常,被 finally 的返回值顶替。
Python 3.14 官方更新文档:

代码语言设计的逻辑上没问题,不代表不需要人为干预。finally 块的逻辑上不存在任何问题,只是人们太容易忽略这个问题且出错了,所以 Python 开发者们通过分析现有项目的使用情况就大胆禁用了 finally 里 return 的语法。而反观其他语言如 Java,到现在都只有人为的规范,并没有强制限制这个特性。这可能就是 Python 越来越好用的原因吧。
部分翻译自 substack 文章,原文放原文链接里了。