Python代码调试技巧:print大法之外,pdb和VS Code断点调试才是正道
调试是编程中不可或缺的技能。掌握有效的调试方法可以大大提高开发效率,快速定位和解决问题。本篇介绍Python常用的调试工具和技巧。
1. print调试
最简单直接的调试方法。
# 基本print调试
def calculate(a, b):
print(f"输入: a={a}, b={b}") # 调试输出
result = a + b
print(f"结果: {result}") # 调试输出
return result
# 使用f-string的=语法(Python 3.8+)
x = 10
y = 20
print(f"{x=}, {y=}") # 输出: x=10, y=20
print(f"{x + y=}") # 输出: x + y=30
# 带位置信息的print
def debug_print(*args, **kwargs):
"""带文件和行号的调试输出"""
import sys
frame = sys._getframe(1)
filename = frame.f_code.co_filename
lineno = frame.f_lineno
print(f"[{filename}:{lineno}]", *args, **kwargs)
debug_print("调试信息") # [script.py:15] 调试信息
# 条件调试输出
DEBUG = True
def dprint(*args, **kwargs):
if DEBUG:
print("[DEBUG]", *args, **kwargs)
dprint("只在DEBUG模式显示")
# 使用pprint美化输出
from pprint import pprint
data = {'name': '张三', 'scores': [90, 85, 92], 'info': {'age': 25, 'city': '北京'}}
pprint(data, indent=2, width=40)
# 输出到stderr(不影响stdout)
import sys
print("错误信息", file=sys.stderr)
2. 断言调试
使用assert进行条件检查。
# 基本断言
def divide(a, b):
assert b != 0, "除数不能为零"
return a / b
# 类型断言
def process_list(data):
assert isinstance(data, list), f"期望list,得到{type(data)}"
assert len(data) > 0, "列表不能为空"
return sum(data)
# 范围断言
def set_age(age):
assert 0 <= age <= 150, f"年龄{age}不在有效范围内"
return age
# 断言与调试
def complex_calculation(x, y, z):
# 前置条件
assert x > 0, "x必须为正数"
assert y != 0, "y不能为零"
intermediate = x / y
assert intermediate < 1000, f"中间结果{intermediate}过大"
result = intermediate * z
# 后置条件
assert result is not None, "结果不应为None"
return result
# 注意:assert可以被禁用
# python -O script.py # 优化模式,assert被忽略
# 不要用assert做输入验证,应该用if和raise
3. pdb调试器
Python内置的交互式调试器。
import pdb
def buggy_function(data):
result = []
for item in data:
pdb.set_trace() # 在此处暂停
processed = item * 2
result.append(processed)
return result
# Python 3.7+ 可以使用breakpoint()
def another_function(x):
breakpoint() # 等同于pdb.set_trace()
return x * 2
# pdb常用命令
"""
h(elp) 显示帮助
n(ext) 执行下一行(不进入函数)
s(tep) 执行下一行(进入函数)
c(ontinue) 继续执行直到下一个断点
r(eturn) 执行到当前函数返回
q(uit) 退出调试器
p expression 打印表达式的值
pp expression 美化打印
l(ist) 显示当前代码
ll 显示当前函数全部代码
w(here) 显示调用栈
u(p) 向上移动调用栈
d(own) 向下移动调用栈
b lineno 在指定行设置断点
b function 在函数入口设置断点
cl(ear) 清除断点
disable/enable 禁用/启用断点
a(rgs) 显示当前函数参数
!statement 执行Python语句
"""
# 条件断点
def process_items(items):
for i, item in enumerate(items):
if i == 5: # 只在第5次迭代时暂停
pdb.set_trace()
process(item)
# 事后调试(程序崩溃后)
import pdb
import traceback
try:
buggy_code()
except Exception:
traceback.print_exc()
pdb.post_mortem() # 在异常点进入调试
# 命令行启动调试
# python -m pdb script.py
4. VS Code调试
VS Code提供强大的图形化调试功能。
// .vscode/launch.json 配置示例
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: 当前文件",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Python: 带参数",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"args": ["--input", "data.txt", "--output", "result.txt"],
"console": "integratedTerminal"
},
{
"name": "Python: 模块",
"type": "debugpy",
"request": "launch",
"module": "mypackage.main",
"console": "integratedTerminal"
},
{
"name": "Python: 远程调试",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
}
}
]
}
# VS Code调试功能
"""
1. 断点
- 点击行号左侧设置断点
- 条件断点:右键断点 -> 编辑断点 -> 输入条件
- 日志点:不暂停,只输出日志
2. 调试控制
- F5: 开始/继续调试
- F10: 单步跳过
- F11: 单步进入
- Shift+F11: 单步跳出
- Ctrl+Shift+F5: 重启调试
- Shift+F5: 停止调试
3. 调试面板
- 变量:查看当前作用域变量
- 监视:添加表达式监视
- 调用堆栈:查看函数调用链
- 断点:管理所有断点
4. 调试控制台
- 可以执行Python表达式
- 查看和修改变量值
"""
# 远程调试设置
import debugpy
# 在远程脚本中添加
debugpy.listen(5678)
print("等待调试器连接...")
debugpy.wait_for_client()
print("调试器已连接")
# 然后在VS Code中使用"远程调试"配置连接
5. 日志调试
使用logging模块进行系统化的调试。
import logging
# 基本配置
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('debug.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 不同级别的日志
logger.debug("调试信息,最详细")
logger.info("一般信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误")
# 记录异常
try:
result = 1 / 0
except Exception:
logger.exception("发生异常") # 自动包含堆栈信息
# 带变量的日志
name = "张三"
age = 25
logger.info("用户信息: name=%s, age=%d", name, age)
logger.info(f"用户信息: {name=}, {age=}") # Python 3.8+
# 函数装饰器:记录函数调用
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"调用 {func.__name__}({args}, {kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"{func.__name__} 返回 {result}")
return result
except Exception as e:
logger.exception(f"{func.__name__} 异常: {e}")
raise
return wrapper
@log_calls
def calculate(a, b):
return a + b
# 临时提高日志级别
def debug_section():
old_level = logger.level
logger.setLevel(logging.DEBUG)
try:
# 调试代码
logger.debug("详细调试信息")
finally:
logger.setLevel(old_level)
# 上下文管理器
from contextlib import contextmanager
@contextmanager
def debug_logging(logger, level=logging.DEBUG):
old_level = logger.level
logger.setLevel(level)
try:
yield
finally:
logger.setLevel(old_level)
with debug_logging(logger):
logger.debug("临时调试信息")
6. 异常追踪
详细的异常信息获取。
import traceback
import sys
# 获取完整堆栈信息
try:
result = 1 / 0
except Exception as e:
# 打印堆栈
traceback.print_exc()
# 获取堆栈字符串
error_info = traceback.format_exc()
print(error_info)
# 获取异常信息
exc_type, exc_value, exc_tb = sys.exc_info()
print(f"类型: {exc_type}")
print(f"值: {exc_value}")
# 遍历堆栈帧
for frame in traceback.extract_tb(exc_tb):
print(f"文件: {frame.filename}")
print(f"行号: {frame.lineno}")
print(f"函数: {frame.name}")
print(f"代码: {frame.line}")
# 自定义异常处理器
def exception_handler(exc_type, exc_value, exc_tb):
"""全局异常处理器"""
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_tb)
return
error_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
# 记录到日志
logging.error(f"未捕获的异常:\n{error_msg}")
# 保存到文件
with open('crash.log', 'a') as f:
f.write(f"\n{'='*50}\n")
f.write(f"时间: {datetime.now()}\n")
f.write(error_msg)
sys.excepthook = exception_handler
# 使用cgitb获取更详细的信息
import cgitb
cgitb.enable(format='text') # 或 'html'
7. 性能分析
找出代码中的性能瓶颈。
import time
import cProfile
import pstats
from functools import wraps
# 简单计时
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} 耗时: {end - start:.4f}秒")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "done"
# 上下文管理器计时
from contextlib import contextmanager
@contextmanager
def timer_context(name="代码块"):
start = time.perf_counter()
yield
end = time.perf_counter()
print(f"{name} 耗时: {end - start:.4f}秒")
with timer_context("数据处理"):
# 要计时的代码
data = [i**2 for i in range(10000)]
# cProfile性能分析
def profile_function():
# 要分析的代码
result = sum(i**2 for i in range(100000))
return result
# 方式1:命令行
# python -m cProfile -s cumtime script.py
# 方式2:代码中使用
profiler = cProfile.Profile()
profiler.enable()
profile_function()
profiler.disable()
# 打印统计
stats = pstats.Stats(profiler)
stats.sort_stats('cumtime')
stats.print_stats(10) # 前10个
# 保存分析结果
profiler.dump_stats('profile.prof')
# 使用line_profiler逐行分析
# pip install line_profiler
# 在函数上添加 @profile 装饰器
# 运行: kernprof -l -v script.py
# 内存分析
# pip install memory_profiler
from memory_profiler import profile as mem_profile
@mem_profile
def memory_intensive():
data = [i for i in range(1000000)]
return sum(data)
# 运行: python -m memory_profiler script.py
8. 常见Bug类型
# 1. 索引错误
lst = [1, 2, 3]
# print(lst[3]) # IndexError
# 正确做法
if len(lst) > 3:
print(lst[3])
# 或使用get方法(字典)
d = {'a': 1}
print(d.get('b', 'default'))
# 2. 类型错误
def add(a, b):
return a + b
# add("1", 2) # TypeError
# 正确做法:类型检查
def add_safe(a, b):
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("参数必须是数字")
return a + b
# 3. 可变默认参数
def append_to(item, lst=[]): # 危险!
lst.append(item)
return lst
# 正确做法
def append_to_safe(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
# 4. 闭包陷阱
funcs = []
for i in range(3):
funcs.append(lambda: i) # 所有函数都返回2
# 正确做法
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # 捕获当前值
# 5. 浮点数精度
print(0.1 + 0.2 == 0.3) # False!
# 正确做法
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
# 6. 字符串不可变
s = "hello"
# s[0] = 'H' # TypeError
# 正确做法
s = 'H' + s[1:]
# 7. 浅拷贝问题
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0][0] = 99
print(original[0][0]) # 99,被修改了!
# 正确做法
import copy
deep = copy.deepcopy(original)
# 8. 循环中修改列表
lst = [1, 2, 3, 4, 5]
# for item in lst:
# if item % 2 == 0:
# lst.remove(item) # 危险!
# 正确做法
lst = [item for item in lst if item % 2 != 0]
9. 调试最佳实践
# 1. 二分法定位问题
def binary_debug():
"""
当不确定问题在哪时:
1. 在代码中间加print/断点
2. 确定问题在前半部分还是后半部分
3. 重复直到定位到具体行
"""
pass
# 2. 最小复现
def create_minimal_example():
"""
创建能复现问题的最小代码:
1. 删除不相关的代码
2. 使用硬编码数据替代外部输入
3. 简化到最小可复现状态
"""
pass
# 3. 橡皮鸭调试法
def rubber_duck_debugging():
"""
向橡皮鸭(或任何人/物)解释代码:
1. 逐行解释代码在做什么
2. 解释为什么这样写
3. 解释期望的行为
往往在解释过程中就能发现问题
"""
pass
# 4. 检查假设
def check_assumptions():
"""
验证你的假设:
- 变量真的是你认为的值吗?
- 函数真的返回了你期望的结果吗?
- 条件真的按你想的执行吗?
"""
x = get_value()
assert x is not None, "x不应该是None"
assert isinstance(x, int), f"x应该是int,实际是{type(x)}"
assert x > 0, f"x应该是正数,实际是{x}"
# 5. 版本控制辅助
def git_bisect():
"""
使用git bisect找出引入bug的提交:
git bisect start
git bisect bad # 当前版本有bug
git bisect good v1.0 # v1.0没有bug
# Git会自动二分查找
git bisect reset # 结束
"""
pass
# 6. 调试检查清单
DEBUG_CHECKLIST = """
□ 错误信息是什么?完整阅读了吗?
□ 哪一行出错?
□ 变量的值是什么?
□ 是否有拼写错误?
□ 是否正确导入了模块?
□ 数据类型正确吗?
□ 边界条件处理了吗?
□ 是否有None值?
□ 文件路径正确吗?
□ 最近改了什么代码?
"""
# 7. 使用类型提示帮助调试
from typing import List, Optional
def process_data(items: List[int], threshold: Optional[int] = None) -> List[int]:
"""类型提示可以帮助IDE发现类型错误"""
if threshold is None:
threshold = 0
return [x for x in items if x > threshold]
10. 总结
🔑 核心要点
| 调试方法 | 适用场景 |
|---|---|
| 快速查看变量值 | |
| assert | 验证假设条件 |
| pdb | 交互式逐步调试 |
| VS Code | 图形化调试,复杂项目 |
| logging | 生产环境,长期调试 |
| profiler | 性能问题定位 |
✅ 学习检查清单
- 掌握print调试技巧
- 会使用pdb基本命令
- 能配置VS Code调试
- 了解logging模块使用
- 能识别常见Bug类型
📖 下一步学习
掌握了调试技巧后,让我们学习代码规范与最佳实践:
常见问题 FAQ
💬 print调试和logging有什么区别?
print是临时的,调试完要删掉。logging是永久的,可以分级(DEBUG/INFO/WARNING/ERROR)、输出到文件、格式化时间戳。正式项目必须用logging。
💬 VS Code调试Python需要什么配置?
安装Python扩展,创建.vscode/launch.json,选择”Python File”模板即可。支持断点、变量查看、调用栈、条件断点,比pdb方便10倍。
� 系列导航
- 上一篇:28 - Python日志解析与报告生成
- 当前:29 - Python代码调试技巧
- 下一篇:30 - Python代码规范与最佳实践