目录

Python 调试技巧与性能分析

学习目标:掌握 Python 调试的多种方法,学会使用性能分析工具定位瓶颈,理解常见性能优化策略。


1. print 调试法

最简单但有效的调试方式,适合快速定位问题。

1.1 基础 print

def calculate_price(items):
    total = 0
    for item in items:
        print(f"处理: {item['name']}, 价格: {item['price']}")
        total += item['price']
        print(f"  累计: {total}")
    print(f"最终总价: {total}")
    return total

items = [
    {"name": "苹果", "price": 5},
    {"name": "香蕉", "price": 3},
    {"name": "橙子", "price": 4}
]
calculate_price(items)

1.2 使用 f-string 调试(Python 3.8+)

x = 42
name = "张三"
numbers = [1, 2, 3]

# = 后自动显示变量名和值
print(f"{x=}")        # x=42
print(f"{name=}")     # name='张三'
print(f"{numbers=}")  # numbers=[1, 2, 3]

# 表达式也支持
print(f"{sum(numbers)=}")  # sum(numbers)=6
print(f"{x * 2=}")         # x * 2=84

2. logging 模块调试

比 print 更灵活,支持级别控制和输出到文件。

import logging

# 配置
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

logger = logging.getLogger(__name__)

def process_order(order_id, items):
    logger.debug(f"开始处理订单 {order_id}")
    logger.debug(f"订单商品: {items}")
    
    try:
        total = sum(item['price'] for item in items)
        logger.info(f"订单 {order_id} 总价: {total}")
        
        if total > 1000:
            logger.warning(f"订单 {order_id} 金额较大: {total}")
        
        return total
    except KeyError as e:
        logger.error(f"订单 {order_id} 数据异常: 缺少字段 {e}")
        return None
    except Exception as e:
        logger.critical(f"订单 {order_id} 处理失败: {e}", exc_info=True)
        return None

# 测试
process_order(1, [{"name": "A", "price": 100}])
process_order(2, [{"name": "B"}])  # 缺少 price

3. pdb 调试器

3.1 交互式调试

import pdb

def buggy_function(data):
    result = []
    for i, item in enumerate(data):
        # 设置断点
        pdb.set_trace()
        
        processed = item * 2
        result.append(processed)
    return result

# 运行后进入交互式调试
# buggy_function([1, 2, 3])

3.2 pdb 常用命令

(pdb) 命令一览

导航:
  n / next       ← 执行下一行(不进入函数)
  s / step       ← 执行下一行(进入函数)
  c / continue   ← 继续执行到下一个断点
  r / return     ← 执行到函数返回
  l / list       ← 显示当前代码上下文
  w / where      ← 显示调用栈

查看:
  p expr         ← 打印表达式值
  pp expr        ← 格式化打印
  a / args       ← 显示当前函数参数
  locals()       ← 显示所有局部变量

断点:
  b line         ← 在指定行设置断点
  b func         ← 在函数入口设置断点
  cl / clear     ← 清除断点
  tbreak         ← 临时断点(触发一次后删除)

其他:
  q / quit       ← 退出调试
  h / help       ← 帮助
  !command       ← 执行 Python 语句

3.3 breakpoint()(Python 3.7+)

def complex_logic(numbers):
    total = 0
    for n in numbers:
        # 自动使用当前配置的调试器(默认 pdb)
        breakpoint()
        if n % 2 == 0:
            total += n
        else:
            total -= n
    return total

# 更优雅的方式:条件断点
def conditional_breakpoint(data):
    for i, item in enumerate(data):
        if item is None:  # 只在特定条件断
            breakpoint()
        process(item)

4. IDE 调试

4.1 VS Code 调试配置

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: 当前文件",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "justMyCode": true
        },
        {
            "name": "Python: pytest",
            "type": "python",
            "request": "launch",
            "module": "pytest",
            "args": ["-v", "--pdb"],
            "console": "integratedTerminal"
        },
        {
            "name": "Python: Django",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/manage.py",
            "args": ["runserver"],
            "django": true
        }
    ]
}

4.2 调试技巧

# 1. 条件断点(在 IDE 中右键断点设置条件)
for i in range(1000):
    result = process(i)
    # IDE 断点条件: i == 500

# 2. 日志断点(不暂停执行,只打印信息)
# VS Code: 右键断点 → Log Message

# 3. 异常断点
# 在异常发生时自动暂停
# VS Code: Breakpoints → Uncaught Exceptions

5. 性能分析

5.1 time 模块

import time

# 基础计时
start = time.perf_counter()

# 执行代码
result = sum(range(1000000))

end = time.perf_counter()
print(f"耗时: {end - start:.6f} 秒")

# 多次测量取平均
def benchmark(func, *args, repeat=1000):
    times = []
    for _ in range(repeat):
        start = time.perf_counter()
        func(*args)
        times.append(time.perf_counter() - start)
    
    avg = sum(times) / len(times)
    min_t = min(times)
    max_t = max(times)
    print(f"平均: {avg*1000:.3f}ms, 最小: {min_t*1000:.3f}ms, 最大: {max_t*1000:.3f}ms")

benchmark(sum, range(10000))
benchmark(sum, list(range(10000)))

5.2 timeit 模块

import timeit

# 测量代码片段
time1 = timeit.timeit("sum(range(100))", number=10000)
print(f"sum(range(100)): {time1:.4f}s")

time2 = timeit.timeit(
    "total = 0\nfor i in range(100): total += i",
    number=10000
)
print(f"手动循环: {time2:.4f}s")

# 命令行使用
# python -m timeit -s "x = list(range(100))" "sum(x)"
# python -m timeit -s "x = list(range(100))" "sum(x)" -n 100000 -r 5

# 对比不同实现
setup = "data = list(range(10000))"
tests = {
    "list comprehension": "[x*2 for x in data]",
    "map": "list(map(lambda x: x*2, data))",
    "for loop": """
result = []
for x in data:
    result.append(x*2)
""",
}

for name, code in tests.items():
    time = timeit.timeit(code, setup=setup, number=1000)
    print(f"{name:25s}: {time:.4f}s")

5.3 cProfile(性能分析器)

import cProfile
import pstats

def slow_function():
    """模拟慢函数"""
    total = 0
    for i in range(1000000):
        total += i
    return total

def fast_function():
    """模拟快函数"""
    return sum(range(1000000))

def main():
    slow_function()
    fast_function()
    slow_function()

# 方式1:直接分析
cProfile.run("main()")

# 方式2:保存到文件
cProfile.run("main()", "profile_output.prof")

# 方式3:格式化输出
stats = pstats.Stats("profile_output.prof")
stats.sort_stats("cumulative")  # 按累计时间排序
stats.print_stats(10)  # 显示前 10 个

# 方式4:命令行
# python -m cProfile -s cumulative script.py
输出解读:
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        3    0.150    0.050    0.150    0.050 script.py:5(slow_function)
        1    0.020    0.020    0.020    0.020 script.py:10(fast_function)

ncalls: 调用次数
tottime: 函数自身执行时间(不含子调用)
percall: 每次调用平均时间
cumtime: 累计时间(含子调用)

5.4 line_profiler(逐行分析)

# 安装
pip install line_profiler
from line_profiler import LineProfiler

def process_data(data):
    result = []
    for item in data:
        squared = item ** 2
        result.append(squared)
    return sum(result)

# 创建分析器
lp = LineProfiler()
lp_wrapper = lp(process_data)

# 运行
lp_wrapper(list(range(10000)))

# 打印结果
lp.print_stats()

# 输出:
# Line #  Hits   Time  Per Hit  % Time  Line Contents
#      1                                  def process_data(data):
#      2     1      5      5.0      0.0      result = []
#      3  10001  30000      3.0     30.0      for item in data:
#      4  10000  40000      4.0     40.0          squared = item ** 2
#      5  10000  20000      2.0     20.0          result.append(squared)
#      6     1   5000   5000.0     10.0      return sum(result)

5.5 memory_profiler(内存分析)

pip install memory-profiler
from memory_profiler import profile

@profile
def memory_heavy_function():
    """内存密集型函数"""
    # 大列表
    big_list = [i * 2 for i in range(1000000)]
    
    # 字典
    big_dict = {i: str(i) for i in range(100000)}
    
    # 处理
    total = sum(big_list)
    
    return total

memory_heavy_function()

# 输出:
# Line #    Mem usage    Increment  Occurrences   Line Contents
#      1     50.0 MiB     50.0 MiB           1   @profile
#      2     50.0 MiB      0.0 MiB           1   def memory_heavy_function():
#      3     65.5 MiB     15.5 MiB           1       big_list = [...]
#      4     73.2 MiB      7.7 MiB           1       big_dict = {...}
#      5     73.2 MiB      0.0 MiB           1       total = sum(big_list)
#      6     73.2 MiB      0.0 MiB           1       return total

6. 常见性能优化

6.1 选择合适的数据结构

import timeit

# 1. 列表 vs 集合(查找)
setup = """
data_list = list(range(10000))
data_set = set(range(10000))
"""

# 列表查找 O(n)
t1 = timeit.timeit("9999 in data_list", setup=setup, number=10000)
# 集合查找 O(1)
t2 = timeit.timeit("9999 in data_set", setup=setup, number=10000)

print(f"列表查找: {t1:.4f}s")
print(f"集合查找: {t2:.4f}s")
# 列表查找通常比集合慢 100+ 倍

# 2. 字符串拼接
setup = "parts = ['hello'] * 100"

# + 拼接(慢)
t1 = timeit.timeit(
    "s = ''; [s := s + p for p in parts]",
    setup=setup, number=1000
)

# join(快)
t2 = timeit.timeit(
    "''.join(parts)",
    setup=setup, number=1000
)

print(f"+ 拼接: {t1:.4f}s")
print(f"join: {t2:.4f}s")

6.2 生成器 vs 列表

import sys

# 列表:全部加载到内存
big_list = [x ** 2 for x in range(1000000)]
print(f"列表内存: {sys.getsizeof(big_list) / 1024 / 1024:.2f} MB")
# ~8 MB

# 生成器:惰性求值
big_gen = (x ** 2 for x in range(1000000))
print(f"生成器内存: {sys.getsizeof(big_gen)} bytes")
# ~200 bytes

# 使用场景
# 需要索引/多次遍历 → 列表
# 只需遍历一次/大数据 → 生成器

6.3 缓存

from functools import lru_cache
import time

# 无缓存:重复计算
def fibonacci_slow(n):
    if n < 2:
        return n
    return fibonacci_slow(n - 1) + fibonacci_slow(n - 2)

# 有缓存:避免重复计算
@lru_cache(maxsize=128)
def fibonacci_fast(n):
    if n < 2:
        return n
    return fibonacci_fast(n - 1) + fibonacci_fast(n - 2)

# 性能对比
start = time.perf_counter()
fibonacci_slow(35)
print(f"无缓存: {time.perf_counter() - start:.4f}s")

start = time.perf_counter()
fibonacci_fast(35)
print(f"有缓存: {time.perf_counter() - start:.4f}s")
# 无缓存可能需要数秒,有缓存几乎瞬间

6.4 局部变量优化

import timeit

# 全局变量访问较慢
GLOBAL_VALUE = 100

def use_global():
    total = 0
    for i in range(10000):
        total += i + GLOBAL_VALUE
    return total

# 局部变量访问更快
def use_local():
    local_value = 100  # 转为局部变量
    total = 0
    for i in range(10000):
        total += i + local_value
    return total

t1 = timeit.timeit(use_global, number=10000)
t2 = timeit.timeit(use_local, number=10000)
print(f"全局变量: {t1:.4f}s")
print(f"局部变量: {t2:.4f}s")
# 局部变量通常快 10-20%

7. 调试技巧总结

调试流程:
1. 复现问题 → 确定触发条件
2. 缩小范围 → 二分法注释代码
3. 添加日志 → print/logging
4. 断点调试 → pdb/IDE
5. 检查假设 → 验证变量值
6. 修复验证 → 编写测试

性能优化流程:
1. 测量 → cProfile/timeit
2. 定位瓶颈 → 找到最慢的部分
3. 优化 → 数据结构/算法/缓存
4. 验证 → 重新测量确认改善
5. 避免过早优化 → " premature optimization is the root of all evil "

8. 小结

工具 用途 命令
print/f-string 快速调试 f"{x=}"
logging 结构化日志 logging.basicConfig()
pdb 交互式调试 pdb.set_trace()
breakpoint() 现代断点 breakpoint()
cProfile 性能分析 cProfile.run()
timeit 微基准测试 timeit.timeit()
line_profiler 逐行分析 @profile
memory_profiler 内存分析 @profile

9. 练习题

  1. 使用 pdb 调试一个有 Bug 的函数,找出问题所在。
  2. 使用 cProfile 分析一段代码,找出最耗时的函数。
  3. 对比列表和集合在不同大小下的查找性能。
  4. 使用 @lru_cache 优化一个递归函数,测量优化效果。

下节预告:我们将学习文档编写和注释规范,让你的代码更易于维护和团队协作。