返回博客

34. 调试技巧:print、assert、logging 与 pdb

34. 调试技巧:print、assert、logging 与 pdb

第三十四篇:调试技巧——print、assert、logging 与 pdb

前言

写代码和调 bug 是程序员的两面。Python 提供了多种调试工具和方法,从最简单的 print() 到专业的 pdb 交互式调试器。本文将从简单到高级,系统讲解 Python 调试的四大武器及其最佳实践。

基本用法

def calculate_total(prices):
    total = 0
    for i, price in enumerate(prices):
        # ❌ 常见错误:在循环中累加错误
        total = price  # Bug: 应该用 += 而不是 =
        print(f"[DEBUG] i={i}, price={price}, total={total}")
    return total

print(calculate_total([10, 20, 30]))
# [DEBUG] i=0, price=10, total=10
# [DEBUG] i=1, price=20, total=20
# [DEBUG] i=2, price=30, total=30
# 30 ← 错误!应该是 60

改进的 print 调试

import traceback

def divide_and_sum(numbers):
    total = 0
    for num in numbers:
        try:
            result = 100 / num
            total += result
            # ✅ 更详细的 debug 输出
            print(f"[DEBUG] num={num:5.1f}, result={result:8.2f}, "
                  f"running_total={total:8.2f}", flush=True)
        except ZeroDivisionError:
            print("[WARN] 跳过零值", flush=True)
    return total

divide_and_sum([10, 0, 20])
# [DEBUG] num= 10.0, result=  10.00, running_total=  10.00
# [WARN] 跳过零值
# [DEBUG] num= 20.0, result=   5.00, running_total=  15.00
# 15.0

调试辅助函数

import sys

def debug_var(name, value):
    """打印变量名和其值"""
    print(f"{name} = {value!r}")

def debug_trace(message, **kwargs):
    """带文件位置信息的跟踪日志"""
    frame = sys._getframe(1)
    filename = frame.f_code.co_filename
    lineno = frame.f_lineno
    print(f"[{filename}:{lineno}] {message}", file=sys.stderr)
    if kwargs:
        for k, v in kwargs.items():
            print(f"  {k} = {v!r}", file=sys.stderr)

class_data = {"a": 1, "b": 2}
debug_trace("正在处理班级数据", count=len(class_data), class_data=class_data)
# [/your/file.py:XX] 正在处理班级数据
#   count = 2
#   class_data = {'a': 1, 'b': 2}

assert —— 断言验证

def process_order(order):
    # 验证前置条件
    assert order is not None, "订单不能为 None"
    assert isinstance(order, dict), f"期望 dict,得到 {type(order).__name__}"
    assert "items" in order, f"缺少 items 字段"
    assert "customer_id" in order, f"缺少 customer_id 字段"
    
    # 验证数据约束
    for item in order["items"]:
        assert item["quantity"] > 0, f"数量必须大于 0: {item['quantity']}"
        assert item["price"] >= 0, f"价格不能为负: {item['price']}"
    
    total = sum(item["quantity"] * item["price"] for item in order["items"])
    assert total > 0, f"总金额应为正数,计算结果: {total}"
    
    return {"order_id": id(order), "total": total, "status": "processed"}

order = {
    "customer_id": "C001",
    "items": [{"name": "键盘", "quantity": 2, "price": 299}]
}
print(process_order(order))
# {'order_id': 12345, 'total': 598.0, 'status': 'processed'}

assert 的注意事项

# ❌ 不要在 assert 中执行有副作用的操作
assert func_that_makes_api_call(), "API 失败"  # release 模式下不会调用!

# ✅ 使用 if + raise 或 logging
result = func_that_makes_api_call()
if not result:
    raise RuntimeError("API 调用失败")

# ⚠️ assert 会被 python -O 优化关闭
# python script.py    # assert 生效
# python -O script.py # assert 被完全移除(不检查条件也不评估表达式)

assert 与单元测试的关系

# test_calc.py
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

# 使用 unittest 运行
# $ python -m pytest test_calc.py -v
# PASSED test_calc.py::test_add

logging 模块——专业的日志记录

基础用法

import logging

# 配置根 logger
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    filename="app.log",  # 输出到文件(不加这行则输出到 stderr)
)

logger = logging.getLogger(__name__)

# 不同级别的日志
logger.debug("详细调试信息,仅在开发环境启用")
logger.info("一般信息,记录程序正常运行流程")
logger.warning("警告信息,表示发生了意外的情况但不影响运行")
logger.error("错误信息,表示发生了错误但不中断程序")
logger.critical("严重错误,可能导致程序无法继续运行")

# 输出:
# 2026-09-09 12:00:00 | DEBUG   | __main__ | 详细调试信息...
# 2026-09-09 12:00:00 | INFO    | __main__ | 一般信息...
# ...

logging 层级结构

import logging

# 设置根 logger 为 WARNING 级别
root_logger = logging.getLogger()
root_logger.setLevel(logging.WARNING)

# 子模块的 logger
database_logger = logging.getLogger("app.database")
auth_logger = logging.getLogger("app.auth")

database_logger.setLevel(logging.DEBUG)
auth_logger.setLevel(logging.INFO)

logger = logging.getLogger(__name__)

# 只有 WARNING 及以上才会输出到 root
database_logger.debug("数据库连接初始化")     # ✅ 输出 (因为设置了 DEBUG)
database_logger.info("执行 SELECT 查询")      # ✅ 输出 (DEBUG > INFO)
database_logger.warning("查询超时")           # ✅ 输出
database_logger.error("连接断开")             # ✅ 输出

auth_logger.info("用户登录成功")              # ✅ 输出
auth_logger.debug("token 验证细节")          # ❌ 不输出 (INFO > DEBUG)

多处理器——同时输出到文件和终端

import logging
import sys

# 创建 logger
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# 清除已有的 handler
logger.handlers.clear()

# Handler 1: 控制台输出(只记录 WARNING 以上)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.WARNING)
console_format = logging.Formatter("[CONSOLE] %(levelname)s: %(message)s")
console_handler.setFormatter(console_format)

# Handler 2: 文件输出(记录所有级别)
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_format = logging.Formatter("%(asctime)s | %(levelname)-8s | %(module)s:%(lineno)d | %(message)s")
file_handler.setFormatter(file_format)

# Handler 3: 错误日志单独文件
error_handler = logging.FileHandler("errors.log", encoding="utf-8")
error_handler.setLevel(logging.ERROR)
error_format = logging.Formatter("%(asctime)s | ERROR | %(name)s | %(filename)s:%(lineno)d | %(funcName)s | %(message)s | Exception: %(exc_info)s")
error_handler.setFormatter(error_format)

# 添加 handlers
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.addHandler(error_handler)

# 测试
logger.debug("这是一条调试信息 — 仅写入文件")
logger.info("这是一条普通信息 — 仅写入文件")
logger.warning("这是一条警告信息 — 同时显示在控制台和文件中")
logger.error("这是一条错误信息 — 三条都写,还会记录异常信息")

自定义日志格式化

import logging
import time

class ColoredFormatter(logging.Formatter):
    """彩色日志格式化器(Windows 可能需要 colorama)"""
    COLORS = {
        logging.DEBUG: "\033[36m",    # 青色
        logging.INFO: "\033[32m",     # 绿色
        logging.WARNING: "\033[33m",  # 黄色
        logging.ERROR: "\033[31m",    # 红色
        logging.CRITICAL: "\033[35m", # 紫色
    }
    RESET = "\033[0m"
    
    def format(self, record):
        color = self.COLORS.get(record.levelno, self.RESET)
        record.levelname = f"{color}{record.levelname:<8}{self.RESET}"
        return super().format(record)

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

handler = logging.StreamHandler()
handler.setFormatter(ColoredFormatter(
    "%(asctime)s | %(levelname)s | %(message)s",
    datefmt="%H:%M:%S",
))
logger.addHandler(handler)

logger.debug("这是调试信息")
logger.warning("这是警告信息")
logger.error("这是错误信息")

实战:完整的应用日志配置

import logging
import logging.handlers
from datetime import datetime

def setup_logging(level="INFO", log_dir="logs"):
    """设置完整的日志配置"""
    import os
    os.makedirs(log_dir, exist_ok=True)
    
    logger = logging.getLogger()
    logger.setLevel(getattr(logging, level.upper()))
    
    # 清除已有 handlers
    logger.handlers.clear()
    
    now = datetime.now().strftime("%Y%m%d")
    
    # 每日轮转的文件 handler
    daily_handler = logging.handlers.RotatingFileHandler(
        filename=f"{log_dir}/app_{now}.log",
        maxBytes=10 * 1024 * 1024,  # 10MB
        backupCount=30,              # 保留 30 个备份
        encoding="utf-8",
    )
    daily_handler.setLevel(logging.DEBUG)
    daily_handler.setFormatter(logging.Formatter(
        "%(asctime)s | %(levelname)-8s | %(name)-20s | %(funcName)-20s:%(lineno)-4d | %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    ))
    
    # 错误文件 handler(独立记录)
    error_handler = logging.handlers.RotatingFileHandler(
        filename=f"{log_dir}/error_{now}.log",
        maxBytes=5 * 1024 * 1024,
        backupCount=7,
        encoding="utf-8",
    )
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(logging.Formatter(
        "%(asctime)s | ERROR | %(name)s | %(message)s\nException: %(exc_text)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    ))
    
    logger.addHandler(daily_handler)
    logger.addHandler(error_handler)
    
    return logger

# 使用
logger = setup_logging("INFO")

logger.info("应用启动")
for i in range(5):
    logger.debug(f"处理批次 {i+1}")

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("发生除零错误,以下是调用栈:")
    # exception 会自动包含 traceback

pdb —— 交互式调试器

基本使用

import pdb

def find_max(lst):
    """查找列表中的最大值"""
    max_val = None
    for idx, val in enumerate(lst):
        pdb.set_trace()  # ← 在这里暂停,进入交互模式
        if max_val is None or val > max_val:
            max_val = val
    return max_val

# 运行后会在 set_trace() 处暂停
# 常用命令:
# n (next)       - 执行下一行
# s (step)       - 进入函数内部
# c (continue)   - 继续执行到下一个断点
# p expr         - 打印表达式值
# pp expr        - 美化打印
# l (list)       - 显示当前代码位置
# w (where)      - 显示调用堆栈
# q (quit)       - 退出调试器
# !statement     - 执行 Python 语句

命令行启动调试

# 方法一:通过命令行参数
python -m pdb script.py arg1 arg2

# 方法二:在代码中插入
# python -c "import pdb; exec(open('script.py').read())"

Python 3.7+ 新特性:breakpoint()

def search_users(users, keyword):
    results = []
    breakpoint()  # ← Python 3.7+ 推荐方式(相当于 pdb.set_trace())
    for user in users:
        if keyword.lower() in user["name"].lower():
            results.append(user)
    return results

# 可以通过 PDB 环境变量自定义调试器
# python -c "import pdb; import sys; sys.breakpointhook = lambda: pdb.set_trace()"

# 也可以临时禁用
# breakpoint(disable=True)

pdb++ —— 增强版调试器

# 安装 pdb++(更好的颜色、语法高亮)
pip install pdbpp

# 自动检测并使用 pdb++
import pdb
pdb = __import__("pdb").set_trace()

ipdb —— IPython 集成的调试器

pip install ipdb

# 在代码中使用
import ipdb; ipdb.set_trace()

# 支持 Tab 补全、语法高亮、IPython 魔法命令

实际调试案例

import traceback

def analyze_sales(sales_data):
    """分析销售数据"""
    total_sales = 0
    region_totals = {}
    top_products = {}
    
    for sale in sales_data:
        try:
            amount = float(sale["amount"])
            product = sale["product"]
            region = sale["region"]
            
            total_sales += amount
            
            # 区域统计
            region_totals[region] = region_totals.get(region, 0) + amount
            
            # 产品统计
            if product not in top_products:
                top_products[product] = {"count": 0, "total": 0}
            top_products[product]["count"] += 1
            top_products[product]["total"] += amount
            
        except (ValueError, KeyError) as e:
            # 打印详细错误信息和上下文
            print(f"[ERROR] 处理销售记录失败: {e}")
            print(f"  原始数据: {sale}")
            print(f"  调用栈: {''.join(traceback.format_stack()[:-1])}")
            continue
    
    return {
        "total_sales": total_sales,
        "region_breakdown": region_totals,
        "product_stats": top_products,
    }

# 模拟数据(含错误数据)
sales_data = [
    {"product": "键盘", "amount": "299", "region": "华东"},
    {"product": "鼠标", "amount": "99", "region": "华南"},
    {"product": "显示器", "amount": "abc", "region": "华北"},  # 无效金额
    {"product": "耳机", "region": "西南"},  # 缺少 amount
    {"product": "摄像头", "amount": "199", "region": "华中"},
]

result = analyze_sales(sales_data)
print(f"总销售额: ¥{result['total_sales']:.2f}")

综合实战:生产环境监控

import logging
import functools
import time
import sys

def trace_calls(func):
    """装饰器:追踪函数调用"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        indent = "  " * (len(sys._current_frames()) % 4)
        args_str = ", ".join(
            [repr(a)[:50] for a in args] +
            [f"{k}={repr(v)[:50]}" for k, v in kwargs.items()]
        )
        logger = logging.getLogger("call_tracer")
        logger.debug(f"{indent}{func.__name__}({args_str})")
        
        start_time = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - start_time
            logger.debug(f"{indent}  → {func.__name__} returned (耗时={elapsed*1000:.1f}ms)")
            return result
        except Exception as e:
            elapsed = time.perf_counter() - start_time
            logger.error(f"{indent}  → {func.__name__} raised {type(e).__name__}: {e}")
            raise
    return wrapper

class PerformanceMonitor:
    """性能监控上下文管理器"""
    
    def __init__(self, operation_name):
        self.operation_name = operation_name
        self.logger = logging.getLogger("perf_monitor")
    
    def __enter__(self):
        self.start_time = time.perf_counter()
        self.logger.info(f"[开始] {self.operation_name}")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        elapsed = time.perf_counter() - self.start_time
        if exc_type:
            self.logger.error(f"[失败] {self.operation_name} ({elapsed*1000:.1f}ms): {exc_val}")
        else:
            self.logger.info(f"[完成] {self.operation_name} ({elapsed*1000:.1f}ms)")
        return False

# 使用示例
@trace_calls
def process_batch(items):
    with PerformanceMonitor(f"处理 {len(items)} 项数据"):
        return [item * 2 for item in items]

process_batch([1, 2, 3, 4, 5])

调试工具对比

工具 适用场景 优点 缺点
print() 快速定位问题 简单直接,无需配置 需要手动清理,不够专业
assert 验证前提条件 简洁,文档化预期行为 生产环境可被忽略(-O)
logging 运行时信息收集 持久化,可分级,可过滤 需要正确配置,性能开销
pdb/breakpoint() 深入排查复杂 bug 交互式,可检查任意变量 需要人工操作,不适合生产
IDE 调试器 日常开发调试 图形化界面,可视化断点 需要 IDE 支持
logging.handlers 大规模日志管理 自动轮转、压缩、远程发送 配置复杂

最佳实践总结

✅ 推荐做法

  1. 开发阶段:先用 print() + assert 快速定位,再用 pdb 深入分析
  2. 正式环境:只用 logging,按级别分类记录
  3. 错误追踪:关键路径使用 logger.exception() 记录完整堆栈
  4. 日志分级:
    • DEBUG: 详细的调试信息(仅开发环境)
    • INFO: 正常流程信息
    • WARNING: 意外但可恢复的情况
    • ERROR: 需要关注的错误
    • CRITICAL: 可能导致崩溃的严重错误
  5. 避免 print:不要在生产代码中留下硬编码的 print 调试语句
  6. 断言不替代输入验证:对于不可信的数据,始终使用 if/raise 而非 assert

❌ 避免做法

# ❌ 把 assert 当输入验证
assert isinstance(data, dict)  # release 时被忽略!
# ✅ 使用 if + raise
if not isinstance(data, dict):
    raise TypeError(f"期望 dict,收到 {type(data).__name__}")

# ❌ 忘记关闭文件或释放资源
f = open("file.txt")
data = f.read()
return data  # 如果中间抛出异常,文件未关闭!
# ✅ 使用 with 语句或 finally
with open("file.txt") as f:
    return f.read()

# ❌ 捕获过于宽泛的异常
try:
    do_something()
except:  # 会吞掉 KeyboardInterrupt、SystemExit!
    pass

# ✅ 精确捕获特定异常
try:
    do_something()
except (TypeError, ValueError) as e:
    logger.error(f"出错了: {e}")

总结

本文系统讲解了 Python 调试的四大核心技能:

  1. print() 调试 — 最简单直接的方法,配合结构化输出提高可读性
  2. assert 断言 — 验证前置条件和不变量,用于开发阶段
  3. logging 模块 — 专业的日志记录方案,支持多级别、多处理器、文件轮转
  4. pdb 调试器 — 交互式断点调试,支持变量检查、代码单步执行
  5. 综合实战 — 性能监控装饰器、批量容错处理、生产环境日志配置
  6. 最佳实践 — 区分开发环境和生产环境的策略

下一篇文章将讲解单元测试与文档测试。


本文是 Python 教程系列的第三十四篇。