🐍 Day 11: 调试与诊断 — 从 print 到 pdb 的进阶之路

📖 知识点简介

代码写出来不叫写完了,调通了才算 。今天的核心是让你掌握一套调试方法论:从最原始的 print 打印、到 assert 断言的防御式检查、再到 logging 的日志体系,最后是 pdb/ipdb 的交互式断点调试。学会这些,90% 的 Bug 你都能亲手解决。


1️⃣ print 调试 --- 最简单也最容易被低估

print 调试的"原罪"是会忘记删。但作为一个快速验证手段,它依然好用。

带标记的 print

python 复制代码
# ❌ 错误的 print 调试
def process(data):
    result = do_something(data)
    print(result)         # 打印完你根本不知道这个值是哪来的
    return result

# ✅ 带标记的 print
def process(data):
    result = do_something(data)
    print(f"[DEBUG] process.result = {result!r}")  # !r 会显示 repr
    return result

!rpprint 让你看清数据结构

python 复制代码
from pprint import pprint

data = {
    "users": [
        {"name": "Alice", "scores": [85, 92, 78]},
        {"name": "Bob", "scores": [None, 88, 95]},  # None 值!
    ]
}

# print 糊成一团
print(data)

# pprint 清晰展示嵌套结构
pprint(data, indent=2, width=40)
# 输出:
# { 'users': [ { 'name': 'Alice',
#                'scores': [85, 92, 78]},
#              { 'name': 'Bob',
#                'scores': [None, 88, 95]}]}

2️⃣ assert --- 防御式编程利器

assert 是写在代码里的"安全检查"------条件不满足就立刻抛出 AssertionError,阻止错误数据继续传播。

基本用法

python 复制代码
def divide(a: int, b: int) -> float:
    assert b != 0, "除数不能为零!"
    assert isinstance(a, (int, float)), f"a 必须是数字,收到 {type(a)}"
    return a / b

实战场景

python 复制代码
def process_batch(items: list[dict]) -> list[dict]:
    """批量处理数据,用 assert 做防御式检查"""
    assert isinstance(items, list), f"需要 list,收到 {type(items)}"
    assert len(items) > 0, "列表不能为空"

    results = []
    for item in items:
        # 断言每个元素的结构
        assert "id" in item, f"缺少 id 字段: {item}"
        assert "value" in item, f"缺少 value 字段: {item}"

        processed = transform(item)
        results.append(processed)

    assert len(results) == len(items), "结果数量不一致"
    return results

🚨 避坑:别过度使用 assert

python 复制代码
# ❌ 不应该用 assert:
# 1. 关键业务逻辑(生产环境可能禁用 assert)
assert is_admin(user), "不是管理员"  # 危险!生产环境可能不报错

# 2. 用户输入校验
assert len(password) > 8, "密码太短"       # 应该用 if + 抛出 ValueError

# 3. 有副作用的表达式
assert db.save(user)  # 关闭优化时不会执行 db.save()

# ✅ assert 的正确场景:
# - 内部不变式检查(这个值应该始终大于 0)
# - 函数前后条件断言(前置条件/后置条件)
# - 开发和测试时的帮助信息

重要python -O 启动时全部 assert 会被跳过!别把 assert 当安全检查用。


3️⃣ logging --- 把 print 升级为专业日志

print 的命运是"用完后删掉,下次又要写"。logging 是它的工业级替代品。

快速上手

python 复制代码
import logging

# 基本配置(只需一次,通常在程序入口做)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)

# 获取 logger(建议每个模块一个)
logger = logging.getLogger(__name__)

# 不同级别
logger.debug("这是调试信息,开发时看")
logger.info("程序正常运行信息")
logger.warning("警告,可能有问题")
logger.error("发生了错误,但程序还能继续")
logger.critical("严重错误,程序可能崩了")

实际对比

python 复制代码
# ❌ print 调试删不干净版
def fetch_data(url):
    print(f"正在请求: {url}")           # 上线前忘了删
    response = requests.get(url)
    print(f"状态码: {response.status_code}")  # 又没删
    return response.json()

# ✅ logging 版本,上线后调整日志级别即可
import logging
logger = logging.getLogger(__name__)

def fetch_data(url):
    logger.debug(f"请求 URL: {url}")
    response = requests.get(url)
    logger.info(f"响应状态: {response.status_code}")
    return response.json()

日志文件 + 控制台双输出

python 复制代码
import logging

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# 控制台 handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# 文件 handler(自动轮转)
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler(
    "app.log", maxBytes=10*1024*1024, backupCount=5, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)

# 统一格式
formatter = logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
console.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 注册到 logger
logger.addHandler(console)
logger.addHandler(file_handler)

# 使用
logger.info("应用启动")

4️⃣ pdb --- 标准库的交互式调试器

print 和 logging 搞不定时,就要上断点调试了。pdb 是 Python 自带的,不需要装任何东西。

标记断点

python 复制代码
def complex_calculation(a, b):
    result = a * b
    import pdb; pdb.set_trace()  # ⚡ 断点!执行到这里停住
    result += 100
    return result

complex_calculation(5, 3)
# 执行到 pdb.set_trace() 时进入交互模式:
# > xxx.py(5)complex_calculation()
# -> result += 100
# (Pdb)

pdb 常用命令速查表

命令 缩写 作用 示例
list l 显示当前行附近的代码 ll 15,25
next n 执行下一行(不进入函数) n
step s 进入函数内部 s
continue c 继续执行到下一个断点 c
print p 打印变量值 p result
pp pp 美化打印 pp data
args a 查看当前函数的参数 a
break b 设置断点 b 15b foo.py:10
clear cl 清除断点 cl 1
where w 查看调用栈 w
up u 上移一层调用栈 u
down d 下移一层调用栈 d
quit q 退出调试器 q
! ! 执行 Python 语句 !result = 999

实战调试会话

python 复制代码
def parse_data(raw):
    items = []
    for i, line in enumerate(raw.split("\n")):
        parts = line.split(",")
        items.append({
            "index": i,
            "name": parts[0],
            "value": int(parts[1]),  # ❌ 这里可能会报错
        })
    return items

# 调试会话
data = "Alice,100\nBob,abc\nCharlie,200"
result = parse_data(data)
text 复制代码
# 在 pdb 中的操作
> ... ValueError: invalid literal for int()...
# 重新运行,加断点
import pdb; pdb.set_trace()  # 放在 parse_data 入口

(Pdb) b 6                   # 在第 6 行(int())设断点
(Pdb) c                      # 继续执行
> ...\parse_data(6)...
(Pdb) p parts                # 打印 parts 变量
['Bob', 'abc']
(Pdb) p parts[1]             # 果然有问题
'abc'
(Pdb) !parts[1] = "200"      # 临时修改变量
(Pdb) c                      # 继续,不再报错

5️⃣ ipdb --- 更好用的 pdb

ipdb 是 IPython 增强版,有语法高亮、Tab 补全、更直观的界面。

bash 复制代码
pip install ipdb   # 或者 uv pip install ipdb
python 复制代码
import ipdb; ipdb.set_trace()
# 用法和 pdb 一样,但体验好太多了!

Python 3.7+ 的内置 breakpoint()

python 复制代码
# 不需要 import 了!
def buggy_function(x):
    result = x * 2
    breakpoint()         # 自动调用 pdb 或你设置的调试器
    return result

设置环境变量可以切换调试器:

bash 复制代码
# 使用 ipdb 代替 pdb
export PYTHONBREAKPOINT=ipdb.set_trace

# 禁用所有 breakpoint
export PYTHONBREAKPOINT=0

6️⃣ 调试综合实战

python 复制代码
import logging
import ipdb
from typing import Optional

logger = logging.getLogger(__name__)

class UserService:
    def get_user_scores(self, user_id: int) -> Optional[list[int]]:
        """获取用户分数,调试模式看全过程"""
        logger.info(f"开始查询用户 {user_id} 的分数")
        try:
            raw_data = self._fetch_from_db(user_id)
            logger.debug(f"原始数据: {raw_data}")

            if raw_data is None:
                logger.warning(f"用户 {user_id} 不存在")
                return None

            scores = self._parse_scores(raw_data)
            logger.info(f"解析完成: {scores}")
            return scores

        except Exception as e:
            logger.error(f"查询失败: {e}", exc_info=True)
            # exc_info=True 会把完整 traceback 写入日志
            raise

    def _parse_scores(self, raw: str) -> list[int]:
        """如果这里出问题,用 breakpoint() 进入调试"""
        breakpoint()  # Python 3.7+ 断点
        return [int(x.strip()) for x in raw.split(",")]

# 运行
service = UserService()
scores = service.get_user_scores(42)

⚡ 避坑 & 要点总结

# 要点 说明
1️⃣ 按优先级选择调试方式 print(快速验证)→ assert(防御检查)→ logging(日常监控)→ pdb/ipdb(疑难杂症)
2️⃣ 生产环境用 logging,别用 print logging 可分级、可写文件、可远程收集
3️⃣ assert 可能被禁用 python -O 时所有 assert 跳过,别用于关键业务检查
4️⃣ breakpoint() 代替 import pdb; pdb.set_trace() 更简洁,可切换调试器,Python 3.7+ 原生支持
5️⃣ 日志记录异常用 exc_info=True 把完整 traceback 写入日志,不丢上下文
6️⃣ pdb 中可以改变量值 !result = 999 或直接赋值,用来继续执行验证
python 复制代码
# 快速测试这段代码的调试行为
def test_me(x):
    y = x * 2
    breakpoint()  # 运行到此处会停住
    z = y + 10
    return z

print(test_me(5))
相关推荐
泡海椒1 小时前
告别代码重启部署:JQuick-Java动态规则加载机制原理与实战
后端
HLeiDev1 小时前
邮箱登录与 Google 登录的账号模型设计与实现
后端
苏三说技术1 小时前
阿里开源了一个神级Agent项目
后端
天衍四九-1 小时前
Agent Skills从入门到工程化(十六):面试中如何讲清楚 Agent Skills?
大数据·数据库·人工智能·python·chatgpt·面试
2601_962077981 小时前
Python中数据可视化的新层次
python·plotly·数据可视化·交互式图表·单行代码
zz-zjx2 小时前
Python实用转换模板
开发语言·前端·python
Python大数据分析@2 小时前
Dan Koe:如何搭建个人品牌?
python
tryCbest2 小时前
Python中__init__.py文件的执行时间
python·fastapi·__init__.py
IT_陈寒2 小时前
Redis莫名连接失败,查了三天居然是配置的锅
前端·人工智能·后端