Skip to content

Python 调试:程序生病了怎么办?

引言:从"瞎猜"到"精准诊断"

想象你身体不舒服:

  • 瞎猜(print():头疼就吃药,脚疼也吃药——症状和病因对不上,病没好还吃出一堆副作用;
  • 体检(assert:去医院做检查,血压、血糖、心电图——指标异常立刻报警,但体检单太多,医生也看不过来;
  • 健康档案(logging:历年体检数据存档,随时可查——不打扰当前生活,需要时调出分析;
  • 专家会诊(pdb / IDE):复杂病情,专家一步步排查——精准但耗时。

Python 调试就是给程序做"体检"——方法从简单到专业,按病情选择


一、调试方法总览:五种"诊疗手段"

方法适用场景优点缺点
print()快速定位简单问题简单直接代码里到处是打印,最后要删
assert验证关键假设失败自动报错太多影响性能,可用 -O 关闭
logging长期监控、生产环境分级输出、可写文件、不用删需要配置
pdb复杂逻辑单步跟踪精确控制命令行操作繁琐
pdb.set_trace()定点排查比全程序单步快还是要命令行
IDE 调试日常开发首选图形界面、直观高效依赖工具

二、print():简单粗暴的"土方法"

python
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 基本用法

python
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() 对比

python
# 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 参数

bash
python -O err.py   # 大写 O,不是数字 0

关闭后,所有 assert 视为 pass——开发时用 assert 保安全,上线时关闭提性能


四、logging:程序的"健康档案"

4.1 基本配置

python
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 zero

4.2 级别控制

级别用途类比
DEBUG详细调试信息体检的每一项指标
INFO关键节点信息挂号、取药记录
WARNING警告,但不影响运行血压偏高,注意
ERROR错误,功能受影响确诊疾病,需要治疗

级别过滤:设置 level=WARNING 后,DEBUGINFO 不输出。

python
logging.basicConfig(level=logging.WARNING)

logging.debug('调试信息')    # 不输出
logging.info('普通信息')     # 不输出
logging.warning('警告信息')  # 输出
logging.error('错误信息')    # 输出

4.3 输出到文件

python
logging.basicConfig(
    level=logging.INFO,
    filename='app.log',      # 写入文件
    format='%(asctime)s - %(levelname)s - %(message)s'
)

logging.info('程序启动')
logging.error('发生错误')

优势:不用删代码,改配置就能控制输出;同时输出到控制台和文件。


五、pdb:命令行"专家会诊"

5.1 全程序单步调试

bash
python -m pdb err.py
python
# err.py
s = '0'
n = int(s)
print(10 / n)

pdb 命令

命令作用类比
l查看当前代码位置看病历
n单步执行下一步检查
p 变量名查看变量值看化验单
c继续执行结束会诊,继续观察
q退出调试结束诊疗

体验:理论万能,但一千行代码要按 999 次 n——太繁琐

5.2 pdb.set_trace():定点断点

python
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 用于数据验证

python
def process_user_input(data):
    assert isinstance(data, str), '数据必须是字符串'   # ❌ 危险!

问题python -O 关闭 assert 后,验证失效,恶意数据直接进入。

修正:用户输入验证用 if + raise,assert 只用于内部逻辑假设

8.2 误区二:logging 级别混乱

python
logging.info('用户点击了按钮')           # ✅ 合适
logging.error('用户点击了按钮')          # ❌ 小题大做
logging.debug('数据库连接池已满')        # ❌ 级别太低,可能漏掉

原则:根据事件严重程度选级别,不是根据"我想不想看到"。

8.3 误区三:生产环境用 print

python
print('用户登录成功')   # ❌ 输出到控制台,无法收集、无法分级

修正:生产环境必须用 logging,支持集中收集和告警。

8.4 误区四:pdb 调多线程/异步代码

pdb 对多线程、异步支持很差,调试这类代码用 logging 或 IDE 的异步调试功能。


九、实际应用案例

案例 1:电商订单状态追踪

python
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:数据处理管道调试

python
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 记录关键变量:

python
def average(scores):
    total = sum(scores)
    count = len(scores)
    return total / count

# 测试
print(average([90, 80, 70]))      # 正常
print(average([]))                 # 这里会崩溃,找出原因并修复
参考答案

问题:空列表时 count = 0total / 0ZeroDivisionError

修复

python
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: 分数列表不能为空!

十一、小结

  1. print():临时排查用,记得删——像创可贴,小伤用用;
  2. assert:验证内部逻辑假设,-O 可关闭——像安检门,异常立刻拦截;
  3. logging:生产环境首选,分级输出、可写文件、不用删——像健康档案,长期有效;
  4. pdb / pdb.set_trace():命令行单步调试,适合复杂逻辑——像电话问诊,精准但慢;
  5. IDE 调试:图形界面,日常开发效率最高——像专家门诊,直观高效;
  6. 终极建议logging 是调试的终极武器——开发时 DEBUG,上线时 INFO,排查时改配置,代码不动。

调试的本质是让程序"说话"——用合适的方式让程序告诉你"我哪里不舒服",而不是瞎猜。