首页 文章 精选 留言 我的

精选列表

搜索[HTTP/3],共10000篇文章
优秀的个人博客,低调大师

一个裸的 except: 让我调试了通宵,这3种写法差距太大了

### **凌晨三点的生产事故** 那天晚上,我刚躺下,手机就炸了。 运维老张在群里连着发了十几条消息:"支付接口超时率飙升到30%了,订单全卡住了,你在吗?" 我瞬间清醒。上个月刚上线的支付模块,出了问题。 远程连上服务器,翻日志。 奇怪的事情发生了——日志里**什么都没有**。没有报错,没有异常堆栈,只有一条条"支付失败"的业务记录。 我把代码翻来覆去看了三遍,逻辑没问题啊。 再往下翻,看到了这段: ``` try: result = third_party_pay(amount, card_info) save_order(result) except: log.error("支付失败") return False ``` 就这么一段。看起来还挺"安全"的——出错了就记日志、返回失败,挺规范吧? **但就是这个看似"安全"的写法,把我坑得通宵没睡。** 因为 `except:` 这个裸捕获,把**所有异常**都吞掉了。包括 `KeyboardInterrupt`、`SystemExit`,也包括最要命的——**内存不足时的 `MemoryError`**。 支付接口调用失败了,但失败的真实原因——对方接口返回了一个特殊格式的异常,我根本没捕获到。那个异常里面,其实带着"银行卡已过期"的具体原因。 但 `except:` 不管三七二十一,全吞了。我只看到"支付失败"四个字,然后去查日志,日志里什么详细信息都没有。 **那个通宵,我什么都没干成。就因为多写了三个字符——`:` 前面没有指定异常类型。** 天亮的时候,我把那段代码改成了这样: ``` try: result = third_party_pay(amount, card_info) save_order(result) except PaymentTimeoutError: log.error("支付超时,稍后重试") except InvalidCardError as e: log.error(f"卡号无效:{e.detail}") return False except Exception as e: log.error(f"未知支付异常:{e}") log.exception(e) return False ``` 问题当场定位:卡号格式不对。用户输错了一个数字。前端校验没拦住,后端裸 `except` 又给吞了。 从那天起,我发誓再也不写裸 `except` 了。 ### **裸 except 到底错在哪里?** 来认识一下Python里三种最常见的异常捕获写法: ``` # 写法一:裸 except(最危险) try: do_something() except: print("出错了") # 写法二:捕获 Exception(比较安全) try: do_something() except Exception: print("出错了") # 写法三:捕获具体异常(最推荐) try: do_something() except ValueError: print("值错了") ``` 表面上看,写法一和写法二好像差不多——都是"出错了就执行"。 **但差太多了。** 关键在于:**Python的异常体系是分层的**。 ``` BaseException ├── SystemExit ├── KeyboardInterrupt ├── GeneratorExit └── Exception ├── ValueError ├── TypeError ├── AttributeError ├── KeyError ├── RuntimeError └── ... (几十种内置异常) ``` `except:` 捕获的是**所有继承自 `BaseException` 的异常**。 而 `except Exception:` 捕获的是**所有继承自 `Exception` 的异常**。 区别在哪?`SystemExit`、`KeyboardInterrupt`、`GeneratorExit` 这三个直接继承自 `BaseException`,**不继承自 `Exception`**。 什么意思? - `KeyboardInterrupt` 是你按 `Ctrl+C` 时抛出的异常 - `SystemExit` 是 `sys.exit()` 时抛出的异常 如果你的代码里用了裸 `except`,按 `Ctrl+C` 都退不出去——因为异常被吞了,程序会继续执行。 这不是开玩笑。我曾经见过一个脚本,因为写了个裸 `except`,运维按了半天 `Ctrl+C` 都关不掉,最后只能 `kill -9` 强杀进程。 ### **吞掉异常的真实代价** 裸 `except` 最大的问题不是捕获了什么,而是**你根本不知道捕获了什么**。 看这段代码: ``` try: result = json.loads(response.text) except: result = {} ``` 看起来挺合理——解析JSON,失败了就用空字典。 但如果 `response` 是 `None`,`response.text` 会抛出 `AttributeError`。被 `except` 吞了。 如果 `response.text` 是 `""`,`json.loads("")` 会抛出 `JSONDecodeError`。又被吞了。 如果内存不够,抛出 `MemoryError`。照样被吞。 你拿到一个空字典,然后继续往下执行。后续的业务逻辑就建立在"这是个空字典"的假设上。 **你调试的时候,看到的不是 `JSONDecodeError`,而是一堆莫名其妙的 `KeyError` 和 `TypeError`。** 真正的异常被吞掉了,吐出来的是一个面目全非的"症状"。 这就是为什么我那个通宵什么都没查到——真实异常被吞了,日志里只有四个字"支付失败"。 ### **三种正确的写法** **写法一:捕获具体异常** ``` try: with open('config.json') as f: config = json.load(f) except FileNotFoundError: print("配置文件不存在,使用默认配置") config = DEFAULT_CONFIG except json.JSONDecodeError as e: print(f"配置文件格式错误:{e}") raise ``` 这是最推荐的写法——**你明确知道可能出什么错,而且每种错误都有对应的处理方式**。 **写法二:捕获 Exception 并记录详细信息** 当你确实需要捕获"所有可能的异常"时(比如在框架的顶层),用 `except Exception` 而不是裸 `except`: ``` try: result = call_external_service() except Exception as e: # 记录完整的堆栈信息 log.error(f"调用外部服务失败:{e}") log.exception(e) # 这行会把完整堆栈打出来 # 返回一个安全的默认值,或者降级处理 return DEFAULT_RESULT ``` 关键点:**捕获了,就一定要记录。** 而且要用 `log.exception` 或者 `traceback.format_exc()`,把堆栈信息留下来。 **写法三:捕获特定异常并重新抛出** 有时候你捕获异常不是为了处理它,而是为了在抛出去之前做点事情——比如记录日志、清理资源: ``` try: result = process_data() except ValueError as e: log.warning(f"数据异常:{e}") # 做完清理工作后,重新抛出 raise except RuntimeError as e: log.error(f"运行时错误:{e}") # 这个太严重了,直接转成业务异常 raise BusinessError("系统繁忙,请稍后重试") from e ``` 注意 `raise` 不加参数表示重新抛出当前异常,不改变堆栈。 ### **还有两个容易被忽略的细节** **细节一:`except` 是有顺序的** ``` try: do_something() except Exception: print("兜底") except ValueError: print("值错误") # 这行永远不会执行 ``` 因为 `Exception` 是 `ValueError` 的父类,`except Exception` 在前面就把所有 `ValueError` 都截胡了。 **正确的顺序是:具体的在前,通用的在后。** ``` try: do_something() except ValueError: print("值错误") except TypeError: print("类型错误") except Exception: print("其他错误") ``` **细节二:`else` 和 `finally` 的妙用** `try` 后面可以跟 `else` 和 `finally`: ``` try: result = risky_operation() except ValueError: log.error("值异常") else: # 只有没有异常时才会执行 log.info("操作成功,结果是:{result}") finally: # 不管有没有异常,都会执行 cleanup_resources() ``` `else` 适合放"一切正常时才做"的事情,`finally` 适合放"无论如何都要做"的事情——比如关闭文件、释放锁。 **注意:如果你在 `except` 里 `return` 了,`finally` 还是会执行。** `finally` 是真正意义上的"无论如何都会执行"。 ### **实战建议:什么时候用什么** **写工具函数、库代码时:** 抛出具体的异常,不要捕获。让调用者决定怎么处理。 ``` def parse_config(path): with open(path) as f: return json.load(f) # 不捕获异常,让调用者自己去处理 ``` **写业务逻辑时:** 捕获具体异常,做针对性处理。实在需要兜底,用 `except Exception` 并记录日志。 **写框架、中间件、顶层入口时:** 可以有一个 `except Exception` 兜底,但必须**记录完整堆栈**。 ``` def main(): try: run_app() except Exception as e: log.critical(f"程序崩溃:{e}") log.exception(e) sys.exit(1) ``` ### **现在你再看那三种写法** 回过头来,这三种写法差距到底有多大? ``` # 写法一:裸 except except: # 捕获所有异常,包括 Ctrl+C # 你知道捕获了什么吗?不知道。 # 等于在代码里埋了一颗地雷。 ``` ``` # 写法二:except Exception except Exception: # 捕获普通异常,但不影响 Ctrl+C # 至少不会让你关不掉程序 # 但还是太宽泛,建议配合日志使用 ``` ``` # 写法三:except ValueError except ValueError: # 只捕获特定异常 # 你知道发生了什么,也知道该怎么处理 # 其他异常会正常上抛,不会遗漏 ``` 从"埋雷"到"专业",差的只是一个异常类型名。 ### **那天之后** 那个通宵之后,我给自己定了一条铁律: **永远不写裸 `except`。** 如果实在想写,先停一下,问自己三个问题: 1. 这里可能会抛出哪些异常? 1. 每种异常我打算怎么处理? 1. 万一有我没想到的异常,我怎么保证不丢失信息? 想清楚了再下笔。 异常捕获不是为了"让程序不崩溃"——而是**让程序在崩溃的时候,你能立刻知道为什么崩溃,并且用最快的速度修好它。** 那个通宵,我损失了睡眠,但换来了一个教训。 值了。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册