异常处理与 pdb 调试 (Exceptions & Debugging)


章节概述

C 语言程序员习惯用 errno、返回值检查 NULL-1 来排错。Python 走的是完全不同的路:错误通过异常(exception)向上冒泡,直到某个 try/except 块被捕获。本章对比这两种错误处理哲学,教你用 try/except/finally/else 写健壮的代码,用 pdb(Python Debugger)做交互式调试——并把它与 GDB 的操作方式做对比。对于 C 程序员来说,pdb 就是”只差没有 core dump”的 GDB 替代品。

核心理念:C 的错误处理是”检查返回值,传播 errno”,Python 的错误处理是”抛出去,让调用者决定怎么处理”。异常不是故障——它们是控制流的一部分。pdb 则让你在异常爆发时,像 GDB 的 backtrace 一样看到完整的调用栈全景。


第一节:从 errno 到 try/except


1.1 C 风格的错误处理 vs Python 的异常

// C: 每次函数调用都要检查返回值
#include <stdio.h>
#include <errno.h>
#include <string.h>
 
int read_config(const char *path, char *buf, size_t size) {
 FILE *f = fopen(path, "r");
 if (!f) {
 fprintf(stderr, "Cannot open %s: %s\n", path, strerror(errno));
 return -1;
 }
 if (fread(buf, 1, size, f) < size) {
 if (ferror(f)) {
 fprintf(stderr, "Read error: %s\n", strerror(errno));
 fclose(f);
 return -2;
 }
 }
 fclose(f);
 return 0;
}
// 主调方需要:int ret = read_config(...); if (ret < 0) { /* 处理 */ }
# Python: 异常自动向上冒泡,只需在合适的层级捕获
def read_config(path):
 with open(path) as f: # 文件不存在?FileNotFoundError 自动上抛
 return f.read() # 读取失败?IOError 自动上抛
 
# 调用方决定何时处理
try:
 content = read_config('/etc/myapp.conf')
except FileNotFoundError:
 print('Config file not found, using defaults')
 content = 'defaults'
except IOError as e:
 print(f'Read error: {e}')
 raise SystemExit(1)

1.2 完整的异常处理结构

python -c "
def divide(a, b):
 return a / b
 
# 四种异常处理结构
numbers = [(10, 2), (10, 0), (10, 'x')]
 
for a, b in numbers:
 try:
 result = divide(a, b)
 except ZeroDivisionError:
 print(f'{a}/{b}: division by zero')
 except (TypeError, ValueError) as e:
 print(f'{a}/{b}: type error - {e}')
 else:
 print(f'{a}/{b} = {result}') # 无异常时执行
 finally:
 print(' (attempt done)')
"

try/except/else/finally 的执行顺序:

是否有异常tryexceptelsefinally
无异常执行完跳过执行总是执行
有匹配异常跳到 except执行跳过总是执行
有未匹配异常跳到 finally跳过跳过执行后异常继续上抛

1.3 常见内置异常类

graph TB
 BE["BaseException"]
 BE --> SK["SystemExit / KeyboardInterrupt"]
 BE --> EX["Exception"]
 EX --> AE["ArithmeticError → ZeroDivisionError"]
 EX --> LE["LookupError → IndexError, KeyError"]
 EX --> TE["TypeError"]
 EX --> VE["ValueError"]
 EX --> OE["OSError → FileNotFoundError, PermissionError"]
 EX --> NE["NameError"]
 EX --> ATE["AttributeError"]
python -c '
# 对照 C 语言的常见错误
exceptions = {
 "ZeroDivisionError": "除以 0 ← C 中通过检查除数是否为 0 避免",
 "IndexError": "列表索引越界 ← C 中是未定义行为或段错误",
 "KeyError": "字典键不存在 ← C 哈希表中返回 NULL",
 "TypeError": "类型不匹配 ← C 中编译时捕捉(或隐式转换)",
 "ValueError": "值不合理(如 int(\\\"abc\\\"))← C 中 atoi 返回 0",
 "FileNotFoundError": "文件不存在 ← C 中 fopen 返回 NULL",
 "AttributeError": "属性不存在 ← C 中编译时捕捉(struct 字段)",
 "NameError": "变量名未定义 ← C 中编译时捕捉",
}
for ex, desc in exceptions.items():
 print(f"{ex}: {desc}")
'

注意 IndexErrorKeyError 都是 LookupError 的子类。如果你要同时捕获索引错误和键错误,可以写 except LookupError


第二节:raise 与自定义异常


2.1 raise 抛出异常

python -c "
def validate_age(age):
 if not isinstance(age, int):
 raise TypeError(f'Age must be int, got {type(age).__name__}')
 if age < 0:
 raise ValueError(f'Age cannot be negative: {age}')
 if age > 150:
 raise ValueError(f'Age too large: {age}')
 return age
 
# 测试验证
for v in [25, -5, 200, 'thirty']:
 try:
 print(f'Valid: {validate_age(v)}')
 except (TypeError, ValueError) as e:
 print(f'Invalid: {e}')
"

2.2 异常链(Exception Chaining)

python -c "
import json
 
def load_config(path):
 try:
 with open(path) as f:
 return json.load(f)
 except FileNotFoundError as e:
 raise RuntimeError(f'Config not found: {path}') from e
 except json.JSONDecodeError as e:
 raise ValueError(f'Invalid JSON in {path}: {e}') from e
 
# 测试
for path in ['/nonexistent.json', '/tmp/malformed.json']:
 try:
 load_config(path)
 except (RuntimeError, ValueError) as e:
 print(f'{type(e).__name__}: {e}')
 print(f' Caused by: {e.__cause__}')
 print()
" 2>/dev/null

跨平台提示

  • Windows:CMD 使用 2>NUL,PowerShell 使用 2>$null
  • macOS:与 Linux 一致,2>/dev/null

> `raise ... from e` 建立异常链,保留了原始异常作为 `__cause__`。这类似于 C 语言中逐层打印 `errno` 和 `strerror` 的调试日志,但 Python 的异常链是**结构化的**——可以被程序逻辑使用,而不只是日志文本。

2.3 自定义异常类
----------------

```bash
python -c "
class NetworkError(ConnectionError):
 '''网络相关错误'''
 def __init__(self, host, port, message):
 self.host = host
 self.port = port
 super().__init__(f'{host}:{port} - {message}')

class TimeoutError(NetworkError):
 '''网络超时'''
 pass

# 使用自定义异常
try:
 raise TimeoutError('db.example.com', 5432, 'connection timed out')
except NetworkError as e:
 print(f'Network {type(e).__name__}: {e}')
 print(f' host={e.host}, port={e.port}')
"

第三节:traceback —— 读懂错误信息


3.1 阅读 traceback

python -c "
def func_c():
 1 / 0 # ZeroDivisionError
 
def func_b():
 func_c()
 
def func_a():
 func_b()
 
func_a()
" 2>&1 || true

输出解读:

Traceback (most recent call last): ← 从顶层调用方开始
 File "<string>", line 10, in <module> ← 第 10 行调用了 func_a()
 File "<string>", line 8, in func_a ← func_a() 中调用了 func_b()
 File "<string>", line 5, in func_b ← func_b() 中调用了 func_c()
 File "<string>", line 2, in func_c ← func_c() 中 1/0 触发
ZeroDivisionError: division by zero ← 最终异常类型和消息

从下往上读——最底部是”事故现场”,顶部是”始作俑者”的调用链。

3.2 以编程方式获取 traceback

python -c "
import traceback
import sys
 
def risky_function():
 return [1, 2, 3][100]
 
try:
 risky_function()
except IndexError:
 # 获取完整的 traceback 字符串
 tb_str = traceback.format_exc()
 print('=== Formatted traceback ===')
 print(tb_str)
 print('============================')
 
 # 获取 traceback 对象(用于程序分析)
 exc_type, exc_value, exc_tb = sys.exc_info()
 print(f'Exception type: {exc_type.__name__}')
 frames = traceback.extract_tb(exc_tb)
 for frame in frames:
 print(f' File \"{frame.filename}\", line {frame.lineno}, in {frame.name}')
 if frame.line:
 print(f' {frame.line.strip()}')
"

第四节:pdb —— Python 的 GDB


4.1 启动 pdb 的三种方式

# 方式一:命令行直接启动 pdb
python -m pdb my_script.py
 
# 方式二:在代码中插入断点
# 在 Python 3.7+ 中使用 breakpoint()
echo 'x = 42
breakpoint() # ← 执行到此处自动进入 pdb
print(x)' > /tmp/debug_me.py
 
python /tmp/debug_me.py
 
# 方式三:异常发生后进入 pdb
python -m pdb -c continue /tmp/debug_me.py 2>&1 | head

4.2 pdb 核心命令与 GDB 对照表

操作pdbGDB 等价说明
设置断点(函数)b func_namebreak func_namebreak func_name在函数入口设断点
设置断点(行号)b 42break 42在当前文件第 42 行
设置断点(文件:行)b script.py:42break script.c:42指定文件
列出断点bbreakinfo breakpoints
删除断点cleardisable Ndelete N
继续执行ccontinuecontinue运行到下一断点
单步执行(不进函数)nnextnext
单步执行(进函数)sstepstep
返回当前函数rreturnfinish
打印变量p exprpp exprprint exprpp 是美化打印
查看调用栈wwherebtbacktrace
移动栈帧u(上)/ d(下)frame N up downpdb 方向与 GDB 相反
查看源码llllistlist
查看参数aargsinfo args
执行任意 Python!expr!len(variable)
查看类型whatis variable(pdb)ptype variable(GDB)
退出qquitquit
帮助hhelphelp
python -c "
# pdb 调试示例
def fibonacci(n):
 a, b = 0, 1
 for _ in range(n):
 a, b = b, a + b
 return a
 
# breakpoint() # 取消注释即可调试
print(f'fib(10) = {fibonacci(10)}')
"

4.3 调试实战

cat > /tmp/buggy.py << 'PYEOF'
def process(lst):
 result = []
 for item in lst:
 result.append(item / len(lst)) # BUG: 如果 lst 为空?
 return result
 
def main():
 data = [100, 200, 300]
 processed = process(data)
 print('OK:', processed)
 
 # 故意触发空列表
 bad = process([])
 print('Bad:', bad)
 
main()
PYEOF
 
python /tmp/buggy.py 2>&1 | head -20
 
# pdb 调试流程:
# python -m pdb /tmp/buggy.py
# (pdb) b process ← 在 process 函数设断点
# (pdb) c ← 继续执行
# > process(...) ← 第一次进入 process
# (pdb) n ← 逐步执行
# (pdb) p lst ← 打印参数
# (pdb) c ← 继续到第二次调用(空列表)
# (pdb) p len(lst) ← 查看列表长度
# (pdb) q ← 退出

4.4 pdb.post_mortem —— 异常后事分析

python -c "
import pdb
import sys
 
def crash():
 return [][0] # IndexError
 
try:
 crash()
except Exception:
 # 异常发生后进入 pdb
 pdb.post_mortem(sys.exc_info()[2])
" 2>&1 <<< 'q'

pdb.post_mortem() 就像是给 Python 程序做的”尸检”——程序已经崩溃了,但你仍然可以检查崩溃瞬间的所有变量状态。这类似于用 GDB 加载 core dump 文件。


练习

以下题目用于验证本章所学内容:

题号题目链接涉及知识点
P1020导弹拦截https://www.luogu.com.cn/problem/P1020贪心、二分
P1021邮票面值设计https://www.luogu.com.cn/problem/P1021动态规划