4. else 与 finally 子句

异常处理里,else 和 finally 这两个子句,说实话,经常被新手忽略。我刚入行那会儿也觉得它们可有可无——try 加 except 不就够了吗?直到有一次线上服务莫名其妙地挂了,一查日志,发现是资源没释放,那个 finally 子句被我漏掉了。嗯,从那儿以后我再也不敢小看它俩。

你想想看,try 块里代码跑完了,到底哪些情况该走 else,什么时候该走 finally?这俩的执行时机和异常传播机制紧密相关。今天我就跟你好好捋一捋。

try-except-else-finally 执行流程 try 代码块 异常? 是 except 捕获 否 else 执行 finally 始终执行 关键点:无论 try 块是否抛出异常,finally 子句都会执行。 如果 try 块没有异常,先执行 else,再执行 finally。

4.1 else 子句:没有异常才走的路

else 子句放在所有 except 之后,finally 之前。它的逻辑很简单——只有当 try 块里的代码没有抛出任何异常时,才会执行 else。如果出了异常,else 直接跳过。

我个人的习惯是,把那些「只有在不出错时才需要执行的代码」放进 else 块里。比如从数据库查询到数据之后,再去拿关联的缓存信息。这样一眼看过去就知道,这段逻辑不会因为 try 里的异常而意外执行。

try:
    user_data = fetch_user_from_db(user_id)
except UserNotFoundError:
    log.error("用户不存在,返回默认值")
    user_data = default_user()
except DatabaseError:
    log.error("数据库挂了,直接抛")
    raise
else:
    # 只有没异常才走这里
    cache_data = fetch_cache(user_id)
    merge_user_info(user_data, cache_data)
finally:
    close_connection()

你想想看,如果把 else 里的代码直接写在 try 块末尾,效果一样吗?表面上看一样,但 fetch_cache 如果也抛异常,就会被前面的 except 意外捕获。用 else 能把「正常流程」和「异常处理」清晰地隔离开。

4.2 finally 子句:无论如何我都会来

finally 是异常处理的最后一道屏障。不管 try 块是正常执行完、还是被 return 了、还是抛了异常,finally 里的代码一定会执行。哪怕你在 try 里写了 sys.exit(0),finally 也会在进程退出前跑完(除非进程被强制 kill)。

我记得有个项目,同事在 try 块里打开了一个文件句柄,没写 finally 去关闭。结果某次业务逻辑抛出异常,文件一直没释放,到后来系统报 “Too many open files” 直接挂了。事后复盘,就是缺了一个 finally。

⚠️ 警告: 如果 finally 内部也抛出了异常,它会覆盖掉 try 块中的原始异常。实际调试时很容易被误导。我曾经踩过这个坑,最后靠打印 __context__ 才找到真正原因。
try:
    file = open("data.txt", "r")
    process(file.read())
except IOError as e:
    log.error(f"文件操作失败: {e}")
finally:
    file.close()   # 保证文件一定关闭

4.3 else + finally 组合:完整的异常处理模式

一个标准的 try/except/else/finally 组合,能覆盖所有场景。下面的表格帮你快速对比四个子句的执行条件:

子句 有无异常时执行情况 是否影响异常传播 典型用途
try总是先执行异常未捕获则传播主要业务逻辑
except仅当有匹配异常时默认捕获并终止传播处理已知异常
else仅当 try 无异常时不改变异常状态无异常才执行的逻辑
finally总是执行不改变异常,但可间接影响(如 return)资源清理、日志记录
💡 小技巧: 如果 finally 块里写了 return,它会覆盖 try 或 except 里的 return。我在代码 review 时发现过好几次这种隐晦的 bug,建议 finally 里只做清理,不要返回值。

4.4 避坑指南:常见的误区

我见过不少同行在 else 和 finally 上翻车。下面列几条我亲身经历过的坑:

  • else 里误用未定义变量:else 能访问 try 里定义的变量,但如果 try 挂了,变量可能还没赋值。我曾经在 else 里用了 try 中一个可能未赋值的变量,结果 UnboundLocalError 逃逸了。
  • finally 里写 return 导致异常丢失:如果 try 抛出了异常,但 finally 执行了一个 return,那个异常就会被默默吞掉。调试时非常难受。
  • finally 里修改全局状态:你在 finally 里改了某个全局计数器,但异常发生时这个修改可能不符合预期逻辑。建议用 else 去处理正常情况下的状态变化。
  • 嵌套 try 时搞混作用域:多层 try 嵌套时,else 和 finally 的绑定关系容易乱。我个人的习惯是避免超过两层的深度,必要的话提取成函数。

总结一句话: else 给正常流程一个独立的舞台,finally 是无论如何都要谢幕的保镖。用好这俩,你的异常处理代码会干净很多,也少很多隐晦的 bug。