Veloris.
返回索引
问题排错 2026-02-14

Python代码调试技巧:print大法之外,pdb和VS Code断点调试才是正道

2 分钟
522 words

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. 总结

🔑 核心要点

调试方法适用场景
print快速查看变量值
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倍。


系列导航

End of file.