| name | systematic-debugging |
| description | 系统化调试助手,帮助科研人员使用结构化方法定位和修复代码问题。支持4阶段调试流程、根因追踪技术、日志和断点策略、条件等待方法、防御式编程。当用户需要:(1) 调试科研代码错误、(2) 追踪数据分析问题、(3) 定位统计计算错误、(4) 解决实验代码bug、(5) 优化代码性能时触发。关键词:debugging、调试、bug修复、错误追踪、问题定位、troubleshooting。
|
| license | Apache-2.0 |
系统化调试助手
帮助科研人员使用系统化、可重复的方法定位和修复代码问题,避免随机猜测和无效尝试。
核心原则
系统化调试的黄金法则
- 重现优先 - 不能重现的问题无法修复
- 假设驱动 - 基于证据提出假设,而不是猜测
- 隔离问题 - 逐步缩小问题范围
- 验证修复 - 确保问题真正解决,而非表面消失
四阶段调试流程
阶段 1:问题重现 (Reproduce)
目标:可靠地触发问题
步骤 1.1:记录问题症状
## Bug报告模板
### 问题描述
简要描述问题是什么
### 预期行为
代码应该做什么
### 实际行为
代码实际做了什么
### 重现步骤
1. 运行 `python analysis.py`
2. 使用数据集 `data/experiment_001.csv`
3. 观察输出
### 环境信息
- Python版本: 3.11.0
- 操作系统: Ubuntu 22.04
- 关键依赖: numpy==1.24.0, pandas==2.0.0
### 错误消息
ValueError: could not convert string to float: 'NA'
File "analysis.py", line 45, in process_data
### 数据样本
提供最小化的测试数据
步骤 1.2:创建最小可重现示例 (MRE)
def analyze_experiment():
data = load_data()
preprocessed = preprocess(data)
results = complex_analysis(preprocessed)
return results
import pandas as pd
data = pd.DataFrame({
'value': ['1.0', '2.0', 'NA', '4.0']
})
numeric_data = data['value'].astype(float)
创建MRE的原则:
- 移除所有无关代码
- 使用小数据集(< 10行)
- 保持错误可重现
- 独立运行(无外部依赖)
步骤 1.3:固定输入和环境
import numpy as np
import random
np.random.seed(42)
random.seed(42)
test_data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(f"NumPy版本: {np.__version__}")
print(f"测试数据: {test_data}")
重现检查清单:
阶段 2:定位问题 (Locate)
目标:找到导致问题的具体代码行
策略 2.1:二分查找法
def process_data(data):
"""使用打印语句进行二分查找"""
print("开始处理数据")
loaded = load_data(data)
print(f"加载完成: {len(loaded)} 行")
cleaned = clean_data(loaded)
print(f"清理完成: {len(cleaned)} 行")
transformed = transform_data(cleaned)
print(f"转换完成: shape = {transformed.shape}")
results = analyze(transformed)
print(f"分析完成: {results}")
return results
二分法原则:
- 在代码中间添加检查点
- 运行代码观察输出
- 如果问题在检查点前:在前半部分继续二分
- 如果问题在检查点后:在后半部分继续二分
- 重复直到找到问题行
策略 2.2:日志记录
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('debug.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def analyze_data(data):
"""使用日志记录详细信息"""
logger.debug(f"开始分析, 数据形状: {data.shape}")
try:
logger.debug(f"输入数据样本:\n{data.head()}")
mean_value = data['value'].mean()
logger.info(f"计算均值: {mean_value}")
std_value = data['value'].std()
outliers = data[abs(data['value'] - mean_value) > 3 * std_value]
if len(outliers) > 0:
logger.warning(f"发现 {len(outliers)} 个异常值")
return mean_value
except Exception as e:
logger.error(f"分析失败: {str(e)}", exc_info=True)
raise
日志级别使用:
DEBUG: 详细的诊断信息
INFO: 正常的操作信息
WARNING: 警告但不影响运行
ERROR: 错误导致功能失败
CRITICAL: 严重错误导致程序崩溃
策略 2.3:断点调试
import pdb
def problematic_function(data):
pdb.set_trace()
result = process(data)
return result
Jupyter Notebook 调试:
%debug
from IPython.core.debugger import set_trace
set_trace()
VS Code / PyCharm 断点:
- 在行号左侧点击设置断点
- F5 启动调试
- F10 单步执行
- F11 进入函数
- 查看变量面板
策略 2.4:数据检查
import pandas as pd
import numpy as np
def inspect_data(data, name="data"):
"""全面检查数据状态"""
print(f"\n{'='*60}")
print(f"检查 {name}")
print(f"{'='*60}")
print(f"类型: {type(data)}")
print(f"形状: {data.shape if hasattr(data, 'shape') else 'N/A'}")
if isinstance(data, pd.DataFrame):
print(f"\n列: {list(data.columns)}")
print(f"\n数据类型:\n{data.dtypes}")
print(f"\n缺失值:\n{data.isnull().sum()}")
print(f"\n描述性统计:\n{data.describe()}")
print(f"\n前5行:\n{data.head()}")
elif isinstance(data, np.ndarray):
print(f"数据类型: {data.dtype}")
print(f"最小值: {data.min()}")
print(f"最大值: {data.max()}")
print(f"均值: {data.mean()}")
print(f"NaN数量: {np.isnan(data).sum()}")
print(f"Inf数量: {np.isinf(data).sum()}")
else:
print(f"长度: {len(data) if hasattr(data, '__len__') else 'N/A'}")
print(f"样本: {data[:5] if hasattr(data, '__getitem__') else data}")
data = pd.read_csv('data.csv')
inspect_data(data, "原始数据")
processed = preprocess(data)
inspect_data(processed, "处理后数据")
阶段 3:诊断根因 (Diagnose)
目标:理解为什么会出现问题
技术 3.1:假设-验证循环
## 根因分析模板
### 问题症状
ValueError: could not convert string to float: 'NA'
### 假设 1: 数据包含非数值字符串
**证据需求**: 检查数据中的唯一值
**验证代码**:
```python
print(data['value'].unique())
# 输出: ['1.0', '2.0', 'NA', '4.0']
结论: ✅ 确认 - 数据中确实有 'NA' 字符串
假设 2: 数据加载时未正确处理缺失值
证据需求: 检查 read_csv 的 na_values 参数
验证代码:
data = pd.read_csv('data.csv')
data = pd.read_csv('data.csv', na_values=['NA', 'N/A', 'null'])
结论: ✅ 确认 - 未指定 na_values 参数
根本原因
数据加载时未将 'NA' 识别为缺失值,导致被当作字符串处理,
后续类型转换时失败。
#### 技术 3.2:差异分析
```python
# 比较工作版本和问题版本
def compare_versions(working_data, broken_data):
"""比较两个版本的差异"""
print("差异分析:")
# 形状差异
if working_data.shape != broken_data.shape:
print(f"形状不同: {working_data.shape} vs {broken_data.shape}")
# 数据类型差异
if not working_data.dtypes.equals(broken_data.dtypes):
print("\n数据类型差异:")
for col in working_data.columns:
if working_data[col].dtype != broken_data[col].dtype:
print(f" {col}: {working_data[col].dtype} -> {broken_data[col].dtype}")
# 值差异
differences = working_data != broken_data
if differences.any().any():
print(f"\n发现 {differences.sum().sum()} 个不同的值")
print("\n不同值的位置:")
print(differences[differences.any(axis=1)])
技术 3.3:回溯追踪
import traceback
try:
result = problematic_function()
except Exception as e:
print("完整错误追溯:")
traceback.print_exc()
tb = traceback.extract_tb(e.__traceback__)
print("\n调用栈分析:")
for frame in tb:
print(f"\n文件: {frame.filename}")
print(f"函数: {frame.name}")
print(f"行号: {frame.lineno}")
print(f"代码: {frame.line}")
技术 3.4:五个为什么
## 五个为什么分析
### 问题: 统计分析结果不正确
**为什么1**: 为什么结果不正确?
→ 因为p值异常小 (p < 1e-50)
**为什么2**: 为什么p值异常小?
→ 因为样本量被错误计算为100000而不是100
**为什么3**: 为什么样本量计算错误?
→ 因为使用了 len(data) 而不是 len(data.dropna())
**为什么4**: 为什么使用了错误的长度?
→ 因为没有考虑缺失值的影响
**为什么5**: 为什么没有考虑缺失值?
→ 因为没有在数据加载后立即检查数据质量
### 根本原因
缺乏数据质量检查步骤
### 预防措施
在分析前添加数据验证函数
阶段 4:修复和验证 (Fix & Verify)
目标:修复问题并确保不再发生
步骤 4.1:最小化修改
def analyze_data(data):
pass
def analyze_data(data):
numeric_data = pd.to_numeric(data['value'], errors='coerce')
步骤 4.2:添加测试用例
import pytest
import pandas as pd
def test_handle_missing_values():
"""测试缺失值处理"""
test_data = pd.DataFrame({
'value': ['1.0', '2.0', 'NA', '4.0', 'N/A']
})
result = process_data(test_data)
assert result['value'].dtype == 'float64'
assert result['value'].isna().sum() == 2
def test_edge_case_empty_data():
"""测试空数据的边界情况"""
test_data = pd.DataFrame({'value': []})
result = process_data(test_data)
assert len(result) == 0
def test_edge_case_all_na():
"""测试全为缺失值的情况"""
test_data = pd.DataFrame({'value': ['NA', 'NA', 'NA']})
result = process_data(test_data)
assert result['value'].isna().all()
步骤 4.3:回归测试
pytest tests/ -v
pytest tests/ --cov=src --cov-report=html
步骤 4.4:防御式编程
def analyze_data(data):
"""分析数据(带防御性检查)"""
if not isinstance(data, pd.DataFrame):
raise TypeError(f"期望 DataFrame, 得到 {type(data)}")
if 'value' not in data.columns:
raise ValueError("数据必须包含 'value' 列")
if len(data) == 0:
raise ValueError("数据不能为空")
numeric_data = pd.to_numeric(data['value'], errors='coerce')
if numeric_data.isna().all():
raise ValueError("所有值都无法转换为数字")
result = numeric_data.mean()
if not isinstance(result, (int, float)):
raise TypeError(f"结果应为数值, 得到 {type(result)}")
return result
步骤 4.5:记录修复
## 修复记录
### Bug ID: #123
**标题**: ValueError when processing data with 'NA' strings
### 根本原因
数据加载时未将 'NA' 识别为缺失值,导致类型转换失败
### 修复方案
在 `pd.read_csv()` 中添加 `na_values=['NA', 'N/A']` 参数
### 代码变更
```diff
- data = pd.read_csv('data.csv')
+ data = pd.read_csv('data.csv', na_values=['NA', 'N/A', 'null'])
测试
防止复发
- 添加了数据验证函数
- 更新了数据加载文档
- 在代码审查检查清单中添加了此项
修复人: [姓名]
日期: 2026-04-06
---
## 常见问题类型和调试策略
### 类型 1: 数据相关问题
#### 问题:数据格式错误
```python
# 症状:类型转换失败
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
# 诊断工具
def diagnose_data_types(df):
"""诊断数据类型问题"""
print("数据类型检查:")
print(df.dtypes)
print("\n每列的唯一类型:")
for col in df.columns:
unique_types = df[col].apply(type).unique()
print(f"{col}: {unique_types}")
print("\n可能的问题值:")
for col in df.columns:
if df[col].dtype == 'object':
# 尝试转换为数值
try:
pd.to_numeric(df[col])
except:
problem_values = df[col][pd.to_numeric(df[col], errors='coerce').isna()]
if len(problem_values) > 0:
print(f"{col}: {problem_values.unique()[:5]}")
问题:缺失值处理
print(f"缺失值数量: {data.isna().sum()}")
print(f"缺失值比例: {data.isna().mean():.2%}")
clean_data = data.dropna()
filled_data = data.fillna(data.mean())
interpolated_data = data.interpolate()
类型 2: 逻辑错误
问题:循环逻辑错误
for i in range(len(data)):
if data[i] > threshold:
data[i+1] = 0
for i in range(len(data) - 1):
if data[i] > threshold:
data[i+1] = 0
问题:条件逻辑错误
if age >= 18 and age <= 65:
category = 'adult'
elif age >= 65:
category = 'senior'
else:
category = 'minor'
if age < 18:
category = 'minor'
elif age < 65:
category = 'adult'
else:
category = 'senior'
类型 3: 数值计算问题
问题:浮点数精度
a = 0.1 + 0.2
print(a == 0.3)
print(a)
import math
print(math.isclose(a, 0.3))
import numpy as np
print(np.isclose(a, 0.3))
问题:整数除法
result = 5 / 2
result = 5 / 2.0
result = 5 / float(2)
from __future__ import division
类型 4: 性能问题
问题:代码运行缓慢
import time
start = time.time()
result = slow_function(data)
end = time.time()
print(f"执行时间: {end - start:.2f} 秒")
import cProfile
cProfile.run('slow_function(data)')
@profile
def slow_function(data):
pass
调试工具箱
Python 调试工具
python -m pdb script.py
pip install ipdb
ipdb.set_trace()
pip install pudb
pudb.set_trace()
python -m cProfile -o output.prof script.py
python -m pstats output.prof
pip install memory_profiler
python -m memory_profiler script.py
Jupyter Notebook 调试
%pdb
%debug
%time statement
%timeit statement
%lprun -f func func(args)
%memit statement
R 调试工具
debug(function_name)
undebug(function_name)
browser()
traceback()
if (x > 100) browser()
Rprof("output.out")
Rprof(NULL)
summaryRprof("output.out")
调试检查清单
🔍 开始调试前
🔎 调试过程中
✅ 修复后
参考资源