4.4. 调试技巧

掌握高效的调试技巧,能大大提高开发效率。

4.4.1. 打印调试

4.4.1.1. f-string 调试(Python 3.8+)

# Python 3.8+ 的 = 说明符
name = "Alice"
age = 30
score = 95.5

# ❌ 传统方式
print(f"name={name}, age={age}, score={score}")

# ✅ 使用 = 说明符
print(f"{name=}, {age=}, {score=}")
# 输出: name='Alice', age=30, score=95.5

# 支持表达式
items = [1, 2, 3]
print(f"{len(items)=}")
# 输出: len(items)=3

print(f"{items[0] + items[1]=}")
# 输出: items[0] + items[1]=3

4.4.1.2. 漂亮打印

from pprint import pprint, pformat

data = {
    'users': [
        {'name': 'Alice', 'scores': [90, 85, 92]},
        {'name': 'Bob', 'scores': [88, 91, 87]},
    ],
    'metadata': {'version': '1.0', 'count': 2}
}

# 格式化输出
pprint(data, indent=2, width=60)

# 获取格式化字符串
formatted = pformat(data)
print(formatted)

# 用于日志
import logging
logging.debug("Data: %s", pformat(data))

4.4.1.3. 结构化输出

# pip install rich

from rich import print
from rich.console import Console
from rich.table import Table

console = Console()

# 彩色打印
print("[bold red]Error![/bold red] Something went wrong")

# 表格
table = Table(title="Users")
table.add_column("Name", style="cyan")
table.add_column("Score", style="green")
table.add_row("Alice", "90")
table.add_row("Bob", "85")
console.print(table)

# 检查对象
from rich import inspect
inspect(list, methods=True)

4.4.2. 断点调试

4.4.2.1. 内置 breakpoint()

def process_data(items):
    result = []
    for item in items:
        breakpoint()  # Python 3.7+,等价于 import pdb; pdb.set_trace()
        processed = transform(item)
        result.append(processed)
    return result

# 常用 pdb 命令
# n (next) - 下一行
# s (step) - 进入函数
# c (continue) - 继续执行
# p <expr> - 打印表达式
# pp <expr> - 漂亮打印
# l (list) - 显示当前代码
# w (where) - 显示调用栈
# q (quit) - 退出

4.4.2.2. 条件断点

def process_items(items):
    for i, item in enumerate(items):
        if i == 50:  # 只在特定条件下暂停
            breakpoint()
        result = process(item)

# 或使用环境变量
import os
if os.environ.get('DEBUG'):
    breakpoint()

4.4.2.3. ipdb(增强版 pdb)

# pip install ipdb

def buggy_function():
    import ipdb; ipdb.set_trace()
    # ipdb 提供语法高亮、Tab 补全等

# 或设置默认调试器
# 在 ~/.pdbrc 或环境变量 PYTHONBREAKPOINT=ipdb.set_trace

4.4.3. 日志调试

4.4.3.1. 配置日志

import logging

# 基本配置
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

# 使用
logger.debug("Debug message")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")
logger.exception("Exception occurred")  # 自动附带堆栈

# 临时调整级别
logger.setLevel(logging.DEBUG)

4.4.3.2. 日志装饰器

import logging
import functools
import time

def log_call(logger=None):
    """记录函数调用的装饰器"""
    def decorator(func):
        nonlocal logger
        if logger is None:
            logger = logging.getLogger(func.__module__)
        
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            logger.debug(f"Calling {func.__name__}({args}, {kwargs})")
            start = time.time()
            try:
                result = func(*args, **kwargs)
                logger.debug(f"{func.__name__} returned {result} in {time.time()-start:.4f}s")
                return result
            except Exception as e:
                logger.exception(f"{func.__name__} raised {e}")
                raise
        return wrapper
    return decorator

@log_call()
def process(data):
    return data * 2

4.4.4. 追踪执行

4.4.4.1. trace 模块

import trace
import sys

# 追踪执行
tracer = trace.Trace(
    ignoredirs=[sys.prefix, sys.exec_prefix],
    trace=1,  # 打印每行
    count=1   # 统计执行次数
)

tracer.run('main()')

# 获取覆盖率报告
results = tracer.results()
results.write_results(show_missing=True, coverdir='.')

4.4.4.2. sys.settrace

import sys

def trace_calls(frame, event, arg):
    """简单的调用追踪"""
    if event == 'call':
        filename = frame.f_code.co_filename
        funcname = frame.f_code.co_name
        lineno = frame.f_lineno
        print(f"Call: {funcname} in {filename}:{lineno}")
    return trace_calls

# 启用追踪
sys.settrace(trace_calls)

# 你的代码
process_data()

# 禁用追踪
sys.settrace(None)

4.4.5. 性能分析

4.4.5.1. cProfile

import cProfile
import pstats
from io import StringIO

def profile_function(func):
    """性能分析装饰器"""
    def wrapper(*args, **kwargs):
        profiler = cProfile.Profile()
        try:
            return profiler.runcall(func, *args, **kwargs)
        finally:
            stream = StringIO()
            stats = pstats.Stats(profiler, stream=stream)
            stats.sort_stats('cumulative')
            stats.print_stats(20)
            print(stream.getvalue())
    return wrapper

@profile_function
def slow_function():
    # 你的代码
    pass

# 命令行使用
# python -m cProfile -s cumulative script.py

4.4.5.2. timeit

import timeit

# 简单计时
print(timeit.timeit('"-".join(str(n) for n in range(100))', number=10000))

# 比较两种实现
def method1():
    return [i ** 2 for i in range(1000)]

def method2():
    return list(map(lambda x: x ** 2, range(1000)))

print(timeit.timeit(method1, number=1000))
print(timeit.timeit(method2, number=1000))

# 使用 Timer 类
timer = timeit.Timer(method1)
print(timer.autorange())  # 自动确定合适的运行次数

4.4.6. 异常调试

4.4.6.1. 打印完整堆栈

import traceback

try:
    risky_operation()
except Exception as e:
    # 打印完整堆栈
    traceback.print_exc()
    
    # 获取堆栈字符串
    tb_str = traceback.format_exc()
    logger.error(tb_str)
    
    # 获取堆栈列表
    tb_list = traceback.format_exception(type(e), e, e.__traceback__)

4.4.6.2. 自定义异常钩子

import sys
import traceback
import logging

def exception_hook(exc_type, exc_value, exc_traceback):
    """全局异常钩子"""
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return
    
    logging.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))

sys.excepthook = exception_hook

4.4.6.3. 断言调试

# 启用断言(默认启用)
def process(data):
    assert data is not None, "Data cannot be None"
    assert len(data) > 0, f"Data is empty, got {data}"
    
    result = calculate(data)
    assert isinstance(result, dict), f"Expected dict, got {type(result)}"
    
    return result

# 禁用断言(生产环境)
# python -O script.py

4.4.7. 调试工具

4.4.7.1. IDE 调试器

VS Code / PyCharm 提供强大的图形化调试功能:

  • 断点(条件断点、日志断点)

  • 变量检查

  • 调用栈

  • 监视表达式

  • 远程调试

4.4.7.2. 远程调试

# pip install debugpy

import debugpy

# 监听调试连接
debugpy.listen(5678)
print("Waiting for debugger...")
debugpy.wait_for_client()
print("Debugger attached!")

# 然后在 VS Code 中连接到 localhost:5678

4.4.8. 最佳实践

调试流程
  1. 重现问题:创建最小可重现案例

  2. 收集信息:日志、错误消息、堆栈

  3. 形成假设:根据信息推测原因

  4. 验证假设:使用断点、打印、测试

  5. 修复并验证:确保问题真正解决

调试技巧
  1. 二分法:在代码中间加断点,缩小问题范围

  2. 橡皮鸭调试:向他人(或橡皮鸭)解释代码

  3. 休息一下:困住时休息往往能找到新思路

  4. 查看 git diff:如果之前工作正常

  5. 阅读错误消息:Python 的错误消息通常很有帮助