4. else 与 finally 子句
异常处理里,else 和 finally 这两个子句,说实话,经常被新手忽略。我刚入行那会儿也觉得它们可有可无——try 加 except 不就够了吗?直到有一次线上服务莫名其妙地挂了,一查日志,发现是资源没释放,那个 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。