第三十四篇:调试技巧——print、assert、logging 与 pdb
前言
写代码和调 bug 是程序员的两面。Python 提供了多种调试工具和方法,从最简单的 print() 到专业的 pdb 交互式调试器。本文将从简单到高级,系统讲解 Python 调试的四大武器及其最佳实践。
print() 调试法——最简单也最实用
基本用法
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 |
大规模日志管理 | 自动轮转、压缩、远程发送 | 配置复杂 |
最佳实践总结
✅ 推荐做法
- 开发阶段:先用
print()+assert快速定位,再用pdb深入分析 - 正式环境:只用
logging,按级别分类记录 - 错误追踪:关键路径使用
logger.exception()记录完整堆栈 - 日志分级:
DEBUG: 详细的调试信息(仅开发环境)INFO: 正常流程信息WARNING: 意外但可恢复的情况ERROR: 需要关注的错误CRITICAL: 可能导致崩溃的严重错误
- 避免 print:不要在生产代码中留下硬编码的 print 调试语句
- 断言不替代输入验证:对于不可信的数据,始终使用
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 调试的四大核心技能:
- print() 调试 — 最简单直接的方法,配合结构化输出提高可读性
- assert 断言 — 验证前置条件和不变量,用于开发阶段
- logging 模块 — 专业的日志记录方案,支持多级别、多处理器、文件轮转
- pdb 调试器 — 交互式断点调试,支持变量检查、代码单步执行
- 综合实战 — 性能监控装饰器、批量容错处理、生产环境日志配置
- 最佳实践 — 区分开发环境和生产环境的策略
下一篇文章将讲解单元测试与文档测试。
本文是 Python 教程系列的第三十四篇。
