异常与堆栈追踪:从懵圈到精准定位
说实话,我刚入行那会儿,最怕的就是线上程序突然崩了。
日志里就一行 Exception,连个堆栈都看不全。那时候我盯着屏幕,脑子里全是问号——到底哪行代码炸了?数据怎么丢的?
后来我才明白,异常处理不是简单的 try-except 一包了事。它是一门手艺。今天我们就聊聊这门手艺的核心:捕获异常、追踪堆栈、定位问题。
try-except:别把异常吞了
很多新手写代码,喜欢这样:
try:
result = risky_operation()
except Exception:
pass
嗯,这代码看着干净,但问题来了——异常被吞了。你永远不知道里面发生了什么。
我个人习惯是:能捕获具体异常,就别用裸的 Exception。比如:
try:
data = json.loads(raw_string)
except json.JSONDecodeError as e:
logger.error(f"JSON 解析失败: {e}")
raise
这样,你知道是 JSON 格式有问题,而不是什么奇奇怪怪的 AttributeError 或 KeyError。
异常链:别丢了根因
我在项目中遇到过这样一个场景:
一个函数从数据库读取配置,然后解析。如果数据库连接失败,抛 ConnectionError。如果解析失败,抛 ValueError。但有时候,解析失败是因为数据库返回了空数据——这其实是连接问题的连锁反应。
如果只是简单捕获再抛新异常,根因就丢了:
try:
config = fetch_config()
parsed = parse_config(config)
except ValueError as e:
raise RuntimeError("配置解析失败")
你看,fetch_config 里的 ConnectionError 完全消失了。排查时你只会看到 "配置解析失败",然后一脸懵。
正确的做法是用 raise ... from ...:
try:
config = fetch_config()
parsed = parse_config(config)
except ValueError as e:
raise RuntimeError("配置解析失败") from e
这样,异常链就完整了。你看到 RuntimeError 的同时,也能追溯到 ValueError,甚至更底层的 ConnectionError。
raise from None。它会切断异常链,除非你明确知道自己在做什么(比如你想隐藏内部实现细节)。
traceback 模块:自己动手,丰衣足食
有时候,异常被框架捕获了,你拿不到完整的堆栈。或者你想在日志里记录更详细的信息。这时候 traceback 模块就派上用场了。
我常用的几个函数:
| 函数 | 作用 |
|---|---|
traceback.format_exc() |
获取当前异常的格式化堆栈字符串 |
traceback.print_exc() |
直接打印堆栈到 stderr |
traceback.extract_tb() |
提取堆栈的摘要信息(文件名、行号、函数名) |
traceback.format_stack() |
获取当前调用栈的格式化字符串(即使没有异常) |
举个例子,你想在日志里记录完整的异常信息:
import traceback
import logging
logger = logging.getLogger(__name__)
try:
result = dangerous_call()
except Exception:
logger.error("发生异常:\n%s", traceback.format_exc())
这样,日志里不仅有异常类型和消息,还有完整的调用栈。排查问题时,一眼就能看到是哪行代码出的问题。
获取精确的调用栈信息
有时候,你需要在没有异常的情况下,知道当前代码是被谁调用的。比如写一个调试工具,或者记录调用链路。
这时候可以用 inspect 模块:
import inspect
def who_called_me():
frame = inspect.currentframe()
caller_frame = frame.f_back
info = inspect.getframeinfo(caller_frame)
print(f"调用者: {info.filename}:{info.lineno} in {info.function}")
嗯,这招我在写一个性能分析工具时用过。它能精确告诉你每个函数的调用来源,比看日志猜半天靠谱多了。
知识体系一览
下面这张图,帮你理清今天讲的内容:
避坑指南
- 我曾经在代码里写了个大 try-except,把所有异常都捕获了,然后只打印了一个 "Error"。结果线上出了 bug,我花了整整一个下午才找到原因。从那以后,我再也不敢偷懒了。
- 我曾经用
raise from None隐藏了一个底层库的异常,结果同事排查时完全摸不着头脑。后来我们约定:除非是安全敏感信息,否则保留异常链。 - 我曾经在日志里只记录
str(e),没有堆栈。结果异常消息是 "list index out of range",但不知道是哪个列表。现在我的日志模板里,traceback.format_exc()是标配。
好了,异常处理这块,说白了就是一句话:别让信息丢失。捕获异常时留日志,抛出异常时留根因,排查问题时看堆栈。做到这三点,线上出问题你也能从容应对。