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=842. 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"}]) # 缺少 price3. 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 Exceptions5. 性能分析
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_profilerfrom 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-profilerfrom 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 total6. 常见性能优化
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. 练习题
- 使用 pdb 调试一个有 Bug 的函数,找出问题所在。
- 使用 cProfile 分析一段代码,找出最耗时的函数。
- 对比列表和集合在不同大小下的查找性能。
- 使用
@lru_cache优化一个递归函数,测量优化效果。
下节预告:我们将学习文档编写和注释规范,让你的代码更易于维护和团队协作。