查看: 1050|回复: 3

Python异常捕获裸except等10个坑点与最佳实践

[复制链接]
发表于 昨天 12:00 | 显示全部楼层 |阅读模式
异常处理的核心原则可以压成两句话:只捕获你能处理的异常,让不能处理的继续向上传播;错误尽早抛出,捕获尽量延迟。原文总结了七篇异常处理内容后,给出10个高频坑点和检查清单。下面不按原文逐条复述,而是按“错误现象—原因—修正代码—适用场景”重组,代码示例保留原文逻辑并做必要精简。

一、10个常见坑点与修正

1. 裸except什么都吞。裸except会连KeyboardInterrupt和SystemExit一起捕获,用户按Ctrl+C都可能关不掉程序。至少捕获Exception并记录日志:
  1. try:
  2.     do_something()
  3. except Exception as e:
  4.     logger.error(f'操作失败: {e}')
复制代码
这里的关键不是“多写一个Exception”,而是明确捕获边界,避免把退出信号当成业务异常。

2. 捕获了但不处理。except里只写pass会让调用者完全不知道失败。捕获后至少记录日志、使用默认值、重试或重新抛出:
  1. try:
  2.     result = risky_operation()
  3. except ValueError as e:
  4.     logger.warning(f'操作失败,使用默认值: {e}')
  5.     result = default_value
复制代码

3. 捕获范围太宽。一个except Exception包住取用户、建订单、支付三段逻辑,出错后无法判断是用户不存在、库存不足还是支付失败。应按异常类型分别处理:
  1. try:
  2.     user = get_user(user_id)
  3.     order = create_order(user, items)
  4.     payment = process_payment(order)
  5. except UserNotFoundError:
  6.     print('用户不存在')
  7. except InsufficientStockError:
  8.     print('库存不足')
  9. except PaymentFailedError as e:
  10.     print(f'支付失败: {e}')
复制代码
精确捕获的前提是底层能抛出有区分度的异常类型。

4. 在finally中使用return。finally里的return会吞掉try中正在传播的异常:
  1. def bad():
  2.     try:
  3.         return 1 / 0
  4.     finally:
  5.         return '看起来正常'
  6. def good():
  7.     try:
  8.         return 1 / 0
  9.     finally:
  10.         cleanup()
复制代码
finally只做清理,不改变控制流。bad()会返回“看起来正常”,ZeroDivisionError不再向外传播,这是排查时很容易误判的行为。

5. 异常处理中的异常。except块里如果再次写错,原始异常会丢失。例如处理代码引用未定义变量会抛NameError。修正思路是让except块保持简洁,复杂处理放到函数里,并保护处理函数:
  1. try:
  2.     result = 1 / 0
  3. except ZeroDivisionError as e:
  4.     try:
  5.         handle_division_error(e)
  6.     except Exception as handle_error:
  7.         print(f'错误处理也失败了: {handle_error}')
复制代码
必要时用traceback记录原始异常,避免“处理失败”覆盖“最初失败”。

6. 循环中的异常处理位置不对。把所有循环包在一个try里,遇到第一个非法值就会终止后续处理:
  1. data = ['1', '2', 'abc', '3', 'xyz', '4']
  2. numbers = []
  3. for item in data:
  4.     try:
  5.         numbers.append(int(item))
  6.     except ValueError:
  7.         print(f'跳过无效数据: {item}')
复制代码
这样numbers得到[1, 2, 3, 4]。如果业务要求“遇到错误立即停止”,才应把try放到循环外,二者是语义差异。

7. 忘记重新抛出。验证错误可以转成返回结果,但数据库错误如果本层无法恢复,不应吞掉:
  1. def process_request(request):
  2.     try:
  3.         validate(request)
  4.         result = execute(request)
  5.         return result
  6.     except ValidationError as e:
  7.         return {'error': str(e)}
复制代码
数据库错误不在这里捕获,交给上层事务管理处理,避免把可恢复与不可恢复异常混在一起。

8. 忽略异常链。异常转换时只抛新异常,会丢失原始连接错误。使用from保留__cause__:
  1. def business_api():
  2.     try:
  3.         raw_query('SELECT ...')
  4.     except ConnectionError as e:
  5.         raise BusinessError(f'查询失败: {e}') from e
复制代码
这样BusinessError.__cause__指向原始ConnectionError,日志和调试能看到完整因果链。

9. 用异常做流程控制。把KeyError当if-else用既慢也不清晰。dict自带get:
  1. def get_value(data, key):
  2.     return data.get(key)
复制代码
异常应服务于意外情况,而不是替代正常分支。

10. 忘记清理资源。open后如果read抛异常,close不会执行。使用with或finally:
  1. def process_file(path):
  2.     with open(path) as f:
  3.         return f.read()
复制代码
如果受限于旧代码不能用with,就在finally中关闭,并判断文件对象是否已创建。

二、最佳实践
原文给出的黄金法则可以落到七个动作:早抛出,在入口处验证;晚捕获,在真正能处理异常的业务逻辑层或API入口捕获;精确捕获,只为能恢复的异常类型写except;不吞异常,捕获后记录日志、回退、重试或重新抛出;资源释放用with或finally;异常消息要包含可操作信息,例如raise ValueError('年龄必须>0'),而不是“无效输入”;用自定义异常表达领域概念,例如InsufficientBalanceError(current=100, required=500),比通用ValueError更有区分度。

入口验证可以这样写:
  1. def validate_input(data):
  2.     if not data:
  3.         raise ValueError('数据不能为空')
  4.     if 'name' not in data:
  5.         raise ValueError('缺少name字段')
复制代码

异常也要文档化。比如转账函数应在docstring中列出可能抛出的ValueError、InsufficientBalanceError、AccountFrozenError,让调用方知道要处理什么。

三、终极检查清单
所有try都有对应的except或finally;except捕获的是能处理的异常类型;没有被吞掉的异常,每个except都做了记录、回退、重试或重新抛出;资源释放用了with或finally;异常消息清晰有用;异常层次结构合理,项目有自定义异常;关键异常记录到日志。

Python异常处理的哲学是EAFP:先做,再处理意外。但EAFP不等于滥用异常。异常处理是区分“能用的代码”和“健壮的代码”的关键:好的异常处理让程序面对意外时优雅降级,而不是崩溃,也不能把Ctrl+C、原始异常链和资源泄漏一起吞掉。
回复

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: Python异常捕获裸except等10个坑点与最佳实践

这篇整理得很实用,尤其是把“现象—原因—修正—场景”拆开的写法,比单纯罗列规则好记得多。第4条在finally里return会吞掉正在传播的异常,这点特别值得强调,这种写法表面上还“返回正常”,排查时最容易误判。第1条裸except会连KeyboardInterrupt和SystemExit一起吞掉,也是很多脚本按Ctrl+C没反应的根本原因,这两条我觉得可以放在检查清单最前面。 第6条关于try放在循环内还是循环外的说明也很到位,核心是语义差异:是跳过坏数据继续处理,还是遇错立即整体失败,想清楚这个再决定位置就不会写错。第8条用from保留__cause__同样实用,否则日志里只能看到转换后的业务异常,原始连接错误完全丢失,定位问题会绕远路。 另外有两个小地方想再听听你的看法:一是第7条说“本层无法恢复就不捕获”,实际写代码时怎么判断某类异常本层到底能不能恢复,有没有比较可操作的判断标准;二是帖子最后“入口验证可以这样写”那里似乎被截断了,validate_input后面的内容没发出来,方便的话可以补一下。
回复 支持 反对

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: Python异常捕获裸except等10个坑点与最佳实践

这篇整理得很实在,10个坑点基本都踩过。裸except和finally里return这两个印象最深,尤其是finally里return把异常吞掉,第一次遇到时排查了半天才反应过来是控制流被改写了。异常链用from保留__cause__也很关键,不然日志里只剩一个BusinessError,根本看不出根因在哪。循环里try放内还是放外那段例子举得好,这不只是写法差异,确实是业务语义问题,放错位置行为就变了。另外想问下帖子最后是不是没贴完?validate_input那段好像断掉了,方便的话可以补一下完整的入口校验示例,正好想看看早抛出是怎么落到代码里的。感谢分享。
回复 支持 反对

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: Python异常捕获裸except等10个坑点与最佳实践

感谢楼主整理,这十条确实是日常写脚本时最容易踩的,尤其第4条在 finally 里写 return,我自己就遇到过——当时查了半天没想明白为什么除零错误“消失”了,后来才意识到 finally 的 return 会把正在传播的异常直接吞掉。还有第1条裸 except,除了 Ctrl+C 关不掉,有时候连 pytest 的断言失败都被吞进去,排查起来挺折磨。 第3条和第7条也很关键:能精确捕获的前提是底层抛得出有区分度的异常,否则上层只好一个大 except 兜住,后面就没法区分可恢复和不可恢复的情况。异常链用 raise ... from e 保留 __cause__ 这个习惯值得坚持,日志里最好把 exc_info 也带上,不然只剩一句“查询失败”很难定位。 另外想请教一下:在循环里逐条 try 这个写法,如果数据量特别大,异常本身的开销和正常分支相比会不会有点影响?还是说一般量级下可读性优先,没必要提前优化?再次感谢分享,检查清单那部分挺适合贴到团队代码规范里的。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-20 07:56 , Processed in 0.033067 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部