结对项目
结对项目·自动生成小学四则运算题目的命令行程序
一、项目信息
| 这个作业属于哪个课程 | 计科23级34班 |
|---|---|
| 这个作业要求在哪里 | 结对项目 |
| 这个作业的目标 | 熟悉结对项目开发流程,积累团队合作开发的实践经验 |
-
项目成员:吴佳童(3223004472),李恺凝(3223004469)
-
Github 项目地址:https://github.com/Lzephyr-w/Paired-Project
二、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
| Development | 开发 | ||
| · Analysis | · 需求分析 (包括学习新技术) | 100 | 90 |
| · Design Spec | · 生成设计文档 | 50 | 45 |
| · Design Review | · 设计复审 | 30 | 30 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 15 |
| · Design | · 具体设计 | 120 | 120 |
| · Coding | · 具体编码 | 200 | 200 |
| · Code Review | · 代码复审 | 30 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 90 | 90 |
| Reporting | 报告 | ||
| · Test Repor | · 测试报告 | 40 | 45 |
| · Size Measurement | · 计算工作量 | 20 | 25 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 50 | 50 |
| · 合计 | 760 | 795 |
三、效能分析
3.1 性能优化时间
一共花费 40 分钟。
3.2 改进思路
- snakeviz profile_results.prof 分析性能

- 分析结果:


(1)文件I/O操作(最显著)
问题:文件打开操作耗时较多
优化: 批量文件写入、处理检查


(2)模块导入开销
优化:在函数内部导入,避免启动时加载

(3)预编译正则表达式
优化: 避免重复编译,提升性能
- 修改前:
def safe_eval_expression(expr: str) -> Fraction:
# 每次调用都重新编译正则表达式
expr = re.sub(r"(-?\d+)'(\d+)/(\d+)", r"Fraction(\1 * \3 + \2, \3)", expr)
expr = re.sub(r'\b(\d+)/(\d+)\b', r"Fraction(\1, \2)", expr)
expr = re.sub(r'\b(\d+)\b', r"Fraction(\1)", expr)
- 修改后:
# 模块级别预编译,只编译一次
MIXED_FRACTION_PATTERN = re.compile(r"(-?\d+)'(\d+)/(\d+)")
FRACTION_PATTERN = re.compile(r'\b(\d+)/(\d+)\b')
INTEGER_PATTERN = re.compile(r'\b(\d+)\b')
OPERATOR_SPLIT_PATTERN = re.compile(r'([*/])')
PLUS_MINUS_SPLIT_PATTERN = re.compile(r'([+-])')
def safe_eval_expression(expr: str) -> Fraction:
# 使用预编译的正则表达式
expr = MIXED_FRACTION_PATTERN.sub(r"Fraction(\1 * \3 + \2, \3)", expr)
expr = FRACTION_PATTERN.sub(r"Fraction(\1, \2)", expr)
expr = INTEGER_PATTERN.sub(r"Fraction(\1)", expr)
- 改进结果:

- _io.open_code(打开代码文件):
优化前:11次,0.003986s(单次 0.0003623s)
优化后:11次,0.000994s(单次 0.00009081s)
✅性能提升约 75%。 - _imp.create_dynamic(创建动态模块):
优化前:3次,0.003337s(单次 0.001112s)
优化后:3次,0.001038s(单次 0.000346s)
✅性能提升约 69%。
3.3 消耗最大的函数
def build_expression(bound: int, max_depth: int, force_operator=False) -> CalcNode:
四、设计实现过程
4.1 工作流程
- 生成模式:主程序 → 题目生成器 → 表达式构建器 → CalcNode → 文件输出
- 检查模式:主程序 → 文件读取 → 表达式计算器 → 答案比较 → 输出
4.2 代码组织

- 类与函数关系图

- 关键函数流程图

- build_expression 递归构建流程图

五、代码说明
5.1 核心类:CalcNode

设计思路:
- 采用二叉树结构表示算术表达式
- 支持两种节点类型:叶子节点(常量)和内部节点(运算符)
- 实现递归计算和遍历
5.2 关键代码说明
(1)表达式生成核心逻辑
def build_expression(bound: int, max_depth: int, force_operator=False) -> CalcNode:
"""递归构建算术表达式"""
if max_depth <= 0 or (not force_operator and random.random() < 0.3):
return CalcNode(const=make_random_value(bound))
operators = ['+', '-', '*', '/'] # 定义可用的四则运算符
for _ in range(20):
op = random.choice(operators)
# 递归生成左右子表达式(深度减1)
left_expr = build_expression(bound, max_depth - 1)
right_expr = build_expression(bound, max_depth - 1)
if op == '-':
left_val = left_expr.compute()
right_val = right_expr.compute()
if left_val < right_val:
left_expr, right_expr = right_expr, left_expr
try:
# 构造候选表达式节点
candidate = CalcNode(lhs=left_expr, oper=op, rhs=right_expr)
if candidate.op_count() > 3: # 运算符总数不能超过3个
continue
if not candidate.validate_subtraction(): # 所有减法子表达式必须满足 e1 >= e2(防止中间结果为负)
continue
if not candidate.validate_division(): # 所有除法子表达式结果必须是真分数(0 <= result < 1)
continue
result = candidate.compute()
if result < 0: # 最终计算结果不能为负数
continue
return candidate
except (ZeroDivisionError, ValueError):
continue # 如果出现除零或非法操作,跳过此次尝试
return CalcNode(const=make_random_value(bound))
设计思路:
- 递归 + 随机生成
- 减法防负数
- 失败回退
(2)安全表达式计算
def safe_eval_expression(expr: str) -> Fraction:
"""安全计算表达式,完全使用分数运算"""
if not expr or expr.isspace():
raise ValueError("空表达式")
try:
# 清理表达式
expr = expr.strip()
# 处理带分数格式 - 先转换带分数
def replace_mixed_fraction(match):
whole = match.group(1)
numer = match.group(2)
denom = match.group(3)
return f"({whole} + {numer}/{denom})"
expr = MIXED_FRACTION_PATTERN.sub(replace_mixed_fraction, expr)
expr = expr.replace('×', '*').replace('÷', '/')
# 处理普通分数
expr = FRACTION_PATTERN.sub(r"Fraction(\1, \2)", expr)
# 处理整数
expr = INTEGER_PATTERN.sub(r"Fraction(\1)", expr)
# 使用更安全的方式计算
result = eval(expr, {"Fraction": Fraction, "__builtins__": {}})
if isinstance(result, (int, float)):
return Fraction(result).limit_denominator()
return result
# 异常处理
except ZeroDivisionError:
raise ValueError("除零错误")
except (ValueError, SyntaxError) as e:
# 提供更详细的错误信息
raise ValueError(f"表达式语法错误: {e} ")
except Exception as e:
raise ValueError(f"计算错误: {e}")
设计思路:
- 使用 Fraction 实现无限精度分数运算,避免浮点误差
- 分阶段转换表达式
- 对空输入、除零、语法错误等异常情况提供清晰的错误提示,保证程序稳定运行。
六、测试运行
(1)命令行输入 python main.py -n 100 -r 10:


(2)程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计:


(3)单元测试用例:
部分代码:


-
测试结果:
✅ 分数格式化:整数、真分数、带分数、负分数的正确渲染
✅ 表达式计算:基本运算、混合运算、括号优先级、错误处理
✅ 题目生成:随机表达式构建、去重机制、运算规则验证
✅ 答案解析:各种格式的答案解析和错误处理
✅ 边界情况:极端值、大量题目生成、单次运算等 -
集成测试从题目生成 → 文件写入 → 文件读取 → 答案检查 → 评分输出,功能完整,有完善的错误处理和边界情 况处理。能够高效生成大量不重复题目
七、项目小结
7.1 项目成就
- 功能完整覆盖需求:成功实现命令行参数控制,支持生成包含自然数、真分数(含带分数)、运算符(+、-、×、÷)及括号的四则运算题目,同时完成题目与答案的文件存储及答案校验统计功能。
- 严格满足生成约束:生成的题目均符合核心约束,逻辑严谨性并经多轮测试验证。
- 支持大规模场景:程序可稳定生成一万道题目,无内存溢出或性能卡顿,文件读写效率满足实用需求。
- 代码可维护性强:采用模块化设计,逻辑清晰,便于后续扩展或修改。
7.2 出现问题及解决方案
- 问题:在开发小学四则运算题目生成器时,我们遇到了eval函数在处理分数运算时出现浮点数精度问题,导致计算结果不准确。
一开始程序生成的题目在人工验证时完全正确但使用-e -a参数进行自动批改时,出现好几个"错误"题目。
具体错误示例:
题目 11: 3/26 + 23 / 25
程序计算结果: 1.0353846153846153 (浮点数)
正确答案: 673/650 (分数)
原因:
expr = "3/26 + 23 / 25"
result = eval(expr) # 返回浮点数 1.0353846153846153
correct_answer = Fraction(673, 650) # 精确分数
# 浮点数与分数比较,因精度问题导致不相等
- 解决方案:
显式控制数值类型,表达式计算结果直接以Fraction对象存储,与答案文件中的分数字符串解析后的Fraction对象直接比较。
优化后,所有分数运算结果均保持精确,自动批改功能的准确率达到 100%。
7.3 结对感受
- 吴佳童:在这个合作过程中,我们能很快定位好分工,共同商讨好时间分配,我发现对方能灵活分配时间,平衡学业课程上的冲突来完成项目,且在测试代码功能上严谨,能在项目上优化提出更多建议。
- 李恺凝:这次结对项目,我和搭档的协作非常顺畅。从需求分析阶段开始,我们就习惯边讨论边画流程图,合作中遇到分数精度问题时,我们没有各自纠结,而是一起查资料、试方案,最终确定用自定义分数类解决。这次经历不仅让我学会了如何在团队中发挥自己的优势,也从搭档身上学到了对细节的关注和解决问题的耐心。

浙公网安备 33010602011771号