Python 调试:程序生病了怎么办?
引言:从"瞎猜"到"精准诊断"
想象你身体不舒服:
- 瞎猜(
print()):头疼就吃药,脚疼也吃药——症状和病因对不上,病没好还吃出一堆副作用; - 体检(
assert):去医院做检查,血压、血糖、心电图——指标异常立刻报警,但体检单太多,医生也看不过来; - 健康档案(
logging):历年体检数据存档,随时可查——不打扰当前生活,需要时调出分析; - 专家会诊(pdb / IDE):复杂病情,专家一步步排查——精准但耗时。
Python 调试就是给程序做"体检"——方法从简单到专业,按病情选择。
一、调试方法总览:五种"诊疗手段"
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
print() | 快速定位简单问题 | 简单直接 | 代码里到处是打印,最后要删 |
assert | 验证关键假设 | 失败自动报错 | 太多影响性能,可用 -O 关闭 |
logging | 长期监控、生产环境 | 分级输出、可写文件、不用删 | 需要配置 |
pdb | 复杂逻辑单步跟踪 | 精确控制 | 命令行操作繁琐 |
pdb.set_trace() | 定点排查 | 比全程序单步快 | 还是要命令行 |
| IDE 调试 | 日常开发首选 | 图形界面、直观高效 | 依赖工具 |
二、print():简单粗暴的"土方法"
def foo(s):
n = int(s)
print('>>> n = %d' % n) # 打印关键变量
return 10 / n
def main():
foo('0')
main()输出:
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: division by zero问题:程序里到处都是 print,运行结果混杂垃圾信息,上线前还得一个个删掉。
适用:临时排查、快速验证——像创可贴,小伤口用用,大伤口不行。
三、assert:关键节点的"安检门"
3.1 基本用法
def foo(s):
n = int(s)
assert n != 0, 'n is zero!' # 断言:n 必须不等于 0
return 10 / n
def main():
foo('0')
main()输出:
AssertionError: n is zero!原理:assert 条件, 消息 —— 条件为 False 时抛 AssertionError。
3.2 与 print() 对比
# print 版:打印后程序继续跑,错误后面才暴露
n = int(s)
print('>>> n = %d' % n) # 打印了,但 n=0 还是继续执行
return 10 / n # 这里才崩溃
# assert 版:n=0 立刻拦截,不往下走
n = int(s)
assert n != 0, 'n is zero!' # 条件不满足,立刻报错
return 10 / n # 安全执行生活化理解:print 是"我记录一下体温 40 度,但继续上班";assert 是"体温 40 度,立刻送医院"。
3.3 关闭断言:-O 参数
python -O err.py # 大写 O,不是数字 0关闭后,所有 assert 视为 pass——开发时用 assert 保安全,上线时关闭提性能。
四、logging:程序的"健康档案"
4.1 基本配置
import logging
logging.basicConfig(level=logging.INFO) # 设置级别
s = '0'
n = int(s)
logging.info('n = %d' % n) # 记录信息
print(10 / n)输出:
INFO:root:n = 0
Traceback (most recent call last):
...
ZeroDivisionError: division by zero4.2 级别控制
| 级别 | 用途 | 类比 |
|---|---|---|
DEBUG | 详细调试信息 | 体检的每一项指标 |
INFO | 关键节点信息 | 挂号、取药记录 |
WARNING | 警告,但不影响运行 | 血压偏高,注意 |
ERROR | 错误,功能受影响 | 确诊疾病,需要治疗 |
级别过滤:设置 level=WARNING 后,DEBUG 和 INFO 不输出。
logging.basicConfig(level=logging.WARNING)
logging.debug('调试信息') # 不输出
logging.info('普通信息') # 不输出
logging.warning('警告信息') # 输出
logging.error('错误信息') # 输出4.3 输出到文件
logging.basicConfig(
level=logging.INFO,
filename='app.log', # 写入文件
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.info('程序启动')
logging.error('发生错误')优势:不用删代码,改配置就能控制输出;同时输出到控制台和文件。
五、pdb:命令行"专家会诊"
5.1 全程序单步调试
python -m pdb err.py# err.py
s = '0'
n = int(s)
print(10 / n)pdb 命令:
| 命令 | 作用 | 类比 |
|---|---|---|
l | 查看当前代码位置 | 看病历 |
n | 单步执行 | 下一步检查 |
p 变量名 | 查看变量值 | 看化验单 |
c | 继续执行 | 结束会诊,继续观察 |
q | 退出调试 | 结束诊疗 |
体验:理论万能,但一千行代码要按 999 次 n——太繁琐。
5.2 pdb.set_trace():定点断点
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 在这里设断点,自动进入 pdb
print(10 / n)运行后程序在 set_trace() 处暂停,可以用 p 查看变量,用 c 继续。
比全程序单步快,但还是命令行操作。
六、IDE 调试:图形化"专家门诊"
推荐 IDE:
- VS Code + Python 插件(免费)
- PyCharm(专业)
优势:
- 点击行号设断点;
- 鼠标悬停看变量值;
- 单步/步入/步出,按钮操作;
- 调用栈可视化。
生活化理解:pdb 是打电话问诊,IDE 是面对面专家门诊——效率天差地别。
七、知识链条:从症状到确诊
程序出问题了
↓
简单问题 → print() 快速定位
↓
关键假设验证 → assert 拦截
↓
需要长期监控 → logging 记录
↓
复杂逻辑排查 → pdb 单步 / IDE 调试
↓
找到病因,修复代码八、常见误区与避坑指南
8.1 误区一:assert 用于数据验证
def process_user_input(data):
assert isinstance(data, str), '数据必须是字符串' # ❌ 危险!问题:python -O 关闭 assert 后,验证失效,恶意数据直接进入。
修正:用户输入验证用 if + raise,assert 只用于内部逻辑假设。
8.2 误区二:logging 级别混乱
logging.info('用户点击了按钮') # ✅ 合适
logging.error('用户点击了按钮') # ❌ 小题大做
logging.debug('数据库连接池已满') # ❌ 级别太低,可能漏掉原则:根据事件严重程度选级别,不是根据"我想不想看到"。
8.3 误区三:生产环境用 print
print('用户登录成功') # ❌ 输出到控制台,无法收集、无法分级修正:生产环境必须用 logging,支持集中收集和告警。
8.4 误区四:pdb 调多线程/异步代码
pdb 对多线程、异步支持很差,调试这类代码用 logging 或 IDE 的异步调试功能。
九、实际应用案例
案例 1:电商订单状态追踪
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
def create_order(user_id, items):
logging.info(f'用户 {user_id} 创建订单,商品数: {len(items)}')
try:
total = sum(item['price'] * item['count'] for item in items)
logging.debug(f'订单金额计算: {total}')
assert total > 0, '订单金额必须大于 0'
order_id = save_to_db(user_id, items, total)
logging.info(f'订单创建成功: {order_id}')
return order_id
except AssertionError as e:
logging.warning(f'订单验证失败: {e}')
raise
except Exception as e:
logging.error(f'订单创建异常: {e}', exc_info=True)
raise
def save_to_db(user_id, items, total):
# 模拟数据库保存
logging.debug(f'保存订单到数据库: user={user_id}, total={total}')
return 'ORD001'
# 测试
create_order(123, [{'price': 100, 'count': 2}]) # INFO 级别记录
create_order(123, []) # WARNING: 订单金额必须大于 0生活化理解:订单系统像医院——INFO 是挂号记录,DEBUG 是化验单,WARNING 是"指标异常但可观察",ERROR 是"需要立即处理"。
案例 2:数据处理管道调试
import logging
logging.basicConfig(level=logging.DEBUG)
def load_data(file_path):
logging.info(f'加载数据文件: {file_path}')
# 模拟加载
data = [1, 2, 3, 4, 5]
logging.debug(f'原始数据: {data}')
return data
def clean_data(data):
logging.info('清洗数据')
cleaned = [x for x in data if x > 0]
logging.debug(f'清洗后: {cleaned} (移除 {len(data) - len(cleaned)} 条)')
return cleaned
def transform_data(data):
logging.info('转换数据')
result = [x * 2 for x in data]
logging.debug(f'转换后: {result}')
return result
def pipeline(file_path):
data = load_data(file_path)
cleaned = clean_data(data)
result = transform_data(cleaned)
logging.info(f'管道完成,最终数据: {result}')
return result
# 运行
pipeline('data.csv')输出:
INFO:root:加载数据文件: data.csv
DEBUG:root:原始数据: [1, 2, 3, 4, 5]
INFO:root:清洗数据
DEBUG:root:清洗后: [1, 2, 3, 4, 5] (移除 0 条)
INFO:root:转换数据
DEBUG:root:转换后: [2, 4, 6, 8, 10]
INFO:root:管道完成,最终数据: [2, 4, 6, 8, 10]优势:生产环境设 level=INFO,只看关键节点;排查问题时设 DEBUG,看每一步细节——不用改代码,改配置就行。
十、实战练习
练习:用 assert 和 logging 调试函数
下面代码计算平均分,但有个 bug,用 assert 找出问题,并用 logging 记录关键变量:
def average(scores):
total = sum(scores)
count = len(scores)
return total / count
# 测试
print(average([90, 80, 70])) # 正常
print(average([])) # 这里会崩溃,找出原因并修复参考答案
问题:空列表时 count = 0,total / 0 抛 ZeroDivisionError。
修复:
import logging
logging.basicConfig(level=logging.INFO)
def average(scores):
logging.debug(f'输入分数: {scores}')
count = len(scores)
assert count > 0, '分数列表不能为空!'
total = sum(scores)
logging.debug(f'总分: {total}, 人数: {count}')
result = total / count
logging.info(f'平均分: {result:.2f}')
return result
# 测试
print(average([90, 80, 70])) # 正常
print(average([])) # AssertionError: 分数列表不能为空!输出:
INFO:root:平均分: 80.00
Traceback (most recent call last):
...
AssertionError: 分数列表不能为空!十一、小结
print():临时排查用,记得删——像创可贴,小伤用用;assert:验证内部逻辑假设,-O可关闭——像安检门,异常立刻拦截;logging:生产环境首选,分级输出、可写文件、不用删——像健康档案,长期有效;pdb/pdb.set_trace():命令行单步调试,适合复杂逻辑——像电话问诊,精准但慢;- IDE 调试:图形界面,日常开发效率最高——像专家门诊,直观高效;
- 终极建议:
logging是调试的终极武器——开发时 DEBUG,上线时 INFO,排查时改配置,代码不动。
调试的本质是让程序"说话"——用合适的方式让程序告诉你"我哪里不舒服",而不是瞎猜。