结对项目
| 这个作业属于哪个课程 | 计科24级78班-软件工程 |
|---|---|
| 这个作业的要求在哪里 | 结对项目 |
| 这个作业的目标 | 通过结对编程实现四则运算生成与批改程序,掌握模块化设计,熟悉软件开发流程 |
姓名:黄泽欣
学号:3224004268
姓名:艾柯代·吐尔逊
学号:3224004266
Github项目地址:https://github.com/Lilimarlene-hzx/arithmetic_generator
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 10 |
| · Estimate | · 估计这个任务需要多少时间 | 10 |
| Development | 开发 | 335 |
| · Analysis | · 需求分析 (包括学习新技术) | 30 |
| · Design Spec | · 生成设计文档 | 20 |
| · Design Review | · 设计复审 | 10 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 |
| · Design | · 具体设计 | 30 |
| · Coding | · 具体编码 | 180 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 |
| Reporting | 报告 | 35 |
| · Test Repor | · 测试报告 | 15 |
| · Size Measurement | · 计算工作量 | 5 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 |
| · 合计 | 380 |
二、效能分析
本次程序性能优化工作共耗时约 1 小时,主要用于 cProfile 性能分析、热点函数定位、代码重构以及优化后复测验证。
首先在终端运行以下命令,生成优化前的报告 before_opt.prof.
python -m cProfile -o reports/before_opt.prof main.py -n 10000 -r 10 --seed 50
通过对程序运行过程进行剖析,我们发现性能瓶颈主要集中在题目生成阶段,尤其是 generator.py 中的 generate_questions 函数,以及 expression.py 中表达式树的递归计算过程。优化前,程序在生成大量题目时反复执行表达式构造、合法性校验、字符串解析和数值求值,导致大量重复计算,造成总体耗时明显上升。
因此,本次优化的核心思路主要分为三点:
第一,减少重复解析与重复计算,避免生成表达式后又将字符串重新解析回表达式对象;
第二,在 expression.py 中为表达式节点增加缓存,避免同一子树反复执行 evaluate、valid_constraints 和 canonical_key;
第三,固定随机种子,确保优化前后测试在相同输入条件下进行,保证评估结果具有可比性。
修改完成后,在终端运行命令生成优化后的报告 after_opt.prof.
python -m cProfile -o reports/after_opt.prof main.py -n 10000 -r 10 --seed 50
以下为整理后的优化前后性能分析对比图,可见在相同随机种子和相同题目规模下,优化后各热点函数的累计耗时明显下降,整体性能显著提升。

经过优化后,程序在相同测试条件下从约 3.396 秒降低到约 1.590 秒,整体提升约 53%,说明本次性能优化具有明显效果。

这些函数在程序中占用的 CPU 时间最多,尤其是:generator.py 中的 generate_questions(),expression.py 中的递归计算和校验逻辑,这是程序“生成题目时”的核心性能瓶颈,也是这次优化的重点对象。
三、设计实现过程
这个项目适合用“分层 + 递归表达式树”这种结构来实现。整体设计思路是:把“命令行入口” “题目生成” “表达式解析/计算” “分数工具” “判题”分成不同模块,每个模块只负责一类事情,这样后续维护和扩展都比较容易。
3.1 代码组织方式*
| 模块 | 作用 |
|---|---|
| main.py | * 负责命令行参数解析和总入口 * 选择执行“生成题目”还是“批改答案” |
| generator.py | * 负责随机生成算式 * 负责去重和题目写入文件 |
| expression.py | * 负责表达式树的表示、计算、合法性检查和转换 * 这是整个项目最核心的模块 |
| fraction_utils.py | * 负责分数的解析和格式化 * 例如 1/2、2’1/3 这种表示法 |
| grader.py | * 负责批改题目 * 读取题目和答案文件,比较正确率 |
| utils.py | 轻量导出层,方便外部直接引用公共工具 |
3.2 设计中的类与函数
3.2.1 类
这个项目中最核心的类是 Expression,定义在 expression.py。
它的作用是表示一棵表达式树,例如:
- 数字节点
- 运算节点
- 左子树 / 右子树
- 运算符
Expression 需要支持这些能力:
- evaluate:计算数值
- valid_constraints:检查是否符合小学题规则
- text:输出文本表现形式
- canonical_key:生成唯一键,用于去重
此外,内部还有一个 _Parser 类,用来把字符串算式解析成表达式树。
3.2.2 函数
| 函数 | 职责 |
|---|---|
| build_parser | 解析命令行参数 |
| main | 程序主流程入口 |
| generate_questions | 生成题目列表 |
| _leaf | 生成数字节点 |
| _candidate | 递归生成运算表达式 |
| write_questions | 把题目和答案写入文件 |
| grade | 把题目和答案文件批改后生成 Grade.txt |
| parse_expression | 解析字符串表达式 |
| parse_number | 把字符串数字转换成分数 |
| format_fraction | 把分数转换成显示格式 |
它们之间的关系如图:

3.2.3 关键函数流程图
题目生成流程图(generate_questions)

判题流程图(grade)

四、代码说明
这个项目的核心是用“表达式树”来表示算式,并在生成题目和判题时反复计算和校验。最关键的代码集中在三处:入口函数、生成器、表达式解析与计算。
4.1 程序入口 main.py
这个文件是整个程序的入口,主要负责解析命令行参数,并决定执行“生成题目”还是“判题”。
"""主程序入口:负责解析命令行参数并分发“生成题目”或“批改答案”的逻辑。"""
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="小学四则运算题目生成与判题程序")
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("-r", type=int, help="数值范围,生成 0 到 r-1 的题目")
mode.add_argument("-e", help="题目文件")
parser.add_argument("-n", type=int, default=10, help="题目数量,默认 10")
parser.add_argument("-a", help="答案文件,与 -e 一起使用")
parser.add_argument("--seed", type=int, default=None, help="固定随机种子,便于复现生成结果和做性能测试")
return parser
设计思路
- build_parser() 负责定义命令参数,增强程序可扩展性;
- main() 根据参数分支执行不同功能;
- 这样做是典型的命令行程序结构,方便用户直接运行。
4.2 题目生成核心 generator.py
这个文件用于生成题目,并保证每道题都是合法、去重的表达式。
"""题目生成模块:随机生成小学四则运算题,并将题目与答案写入指定文件。"""
def _candidate(limit: int, rng: random.Random, operators: int) -> Expression:
if operators == 0:
return _leaf(limit, rng)
left_count = rng.randrange(operators)
right_count = operators - 1 - left_count
left = _candidate(limit, rng, left_count)
right = _candidate(limit, rng, right_count)
return Expression.operation(rng.choice("+-×÷"), left, right)
def generate_questions(count: int, limit: int, seed: int | None = None) -> list[tuple[str, str]]:
if count < 0 or limit < 1:
raise ValueError("count must be non-negative and range must be positive")
rng = random.Random(seed)
questions: list[tuple[str, str]] = []
keys = set()
while len(questions) < count:
expression = _candidate(limit, rng, rng.randrange(1, 4))
if not expression.valid_constraints():
continue
key = expression.canonical_key()
if key in keys:
continue
keys.add(key)
rendered = expression.text()
questions.append((rendered, format_fraction(expression.evaluate())))
return questions
设计思路
- _leaf() 负责生成数字节点;
- _candidate() 使用递归生成带运算符的表达式树;
- generate_questions() 会循环生成,直到满足题目数量要求;
- keys 集合用于去重,防止出现重复题目;
- valid_constraints() 用于保证题目在规则范围内。
4.3 表达式树核心 expression.py
这是整个项目中最关键的模块。它把算式表示成一棵树,并支持计算、合法性检查等。
"""表达式解析与计算模块:把算式文本转成表达式树,支持计算、优先级和合法性校验。"""
@dataclass(frozen=True)
class Expression:
value: Fraction | None = None
operator: str | None = None
left: Expression | None = None
right: Expression | None = None
def evaluate(self) -> Fraction:
cached = getattr(self, "_eval_cache", None)
if cached is not None:
return cached
if self.is_number:
result = self.value
else:
left = self.left.evaluate()
right = self.right.evaluate()
if self.operator == "+":
result = left + right
elif self.operator == "-":
result = left - right
elif self.operator == "×":
result = left * right
else:
if right == 0:
raise ZeroDivisionError
result = left / right
object.__setattr__(self, "_eval_cache", result)
return result
以及:
def valid_constraints(self) -> bool:
cached = getattr(self, "_valid_cache", None)
if cached is not None:
return cached
if self.is_number:
result = True
else:
if not self.left.valid_constraints() or not self.right.valid_constraints():
result = False
else:
left = self.left.evaluate()
right = self.right.evaluate()
if self.operator == "-" and left < right:
result = False
elif self.operator == "÷":
result = right != 0 and left < right
else:
result = True
object.__setattr__(self, "_valid_cache", result)
return result
设计思路
- Expression 把表达式抽象成树型结构;
- 每个节点都有:
a. 值
b. 运算符
c. 左右子节点 - 这样可以自然地实现:
a. 递归计算
b. 优先级控制
c. 合法性约束
d. 去重 - 后期增加缓存后,性能明显提高,因为相同子树不再重复计算。
4.4 代码设计总结
核心思路是“表达式树 + 递归计算 + 规则校验”,这是这类算术题生成器最自然的实现方式。
这也是为什么性能分析时,最耗时的函数集中在 generate_questions、valid_constraints、evaluate 这些地方:因为它们会在大量递归节点上反复计算和判断。
五、测试运行
我们根据题目要求设计了测试脚本 tests.py ,包含了13个测试用例,具体如下:
| 序号 | 测试函数名 | 作用 |
|---|---|---|
| 1 | test_generate_count_and_range |
验证生成题目的数量为指定值,并检查每道题目和答案都非空。 |
| 2 | test_zero_or_one_limit |
验证在极小范围(如 1 或 2)下,仍能按要求生成指定数量的题目。 |
| 3 | test_no_negative_subtraction |
检查生成的减法题不会出现不合法的负数结果,确保表达式满足约束。 |
| 4 | test_division_result_is_fraction |
验证除法题生成后仍然满足合法性要求,不出现错误的整数除法结果。 |
| 5 | test_operator_count_limit |
检查生成的题目中运算符数量不超过 3,符合题目要求。 |
| 6 | test_no_duplicate_questions |
确认生成题目不能重复,使用表达式唯一键去重。 |
| 7 | test_write_questions_files |
测试生成题目并写入 Exercises_test.txt 和 Answers_test.txt 文件,确认文件正确生成。 |
| 8 | test_parse_fraction_and_mixed_number |
验证真分数和带分数可以被正确解析,例如 3/5、2’3/8。 |
| 9 | test_grade_file_statistics |
测试判题逻辑,检查正确题目统计和错误数量统计是否符合预期。 |
| 10 | test_cli_requires_range |
验证命令行参数校验:缺少 -r 时程序应报错并输出帮助信息。 |
| 11 | test_cli_generation_and_seed_reproducibility |
检查固定随机种子后生成结果可重复,确保程序具备可复现性。 |
| 12 | test_parse_expression_on_complex_case |
验证复杂表达式如 3 × (1/2 + 1/4) 能正确计算为 9/4。 |
| 13 | test_large_scale_generation |
测试大规模生成能力,确认能生成 10000 道题目且数量正确。 |
而我们之所以能够程序正确,关键原因是:
- 题目生成结果受随机种子控制,可复现;
- 题目合法性被显式验证;
- 分数和带分数以精确分数表示,不存在浮点误差;
- 判题文件统计与实际题目答案一致;
- 大规模生成和 CLI 交互都经过实际执行验证。
这说明程序不仅能“跑起来”,而且在逻辑上符合题目要求。
六、PSP表格
| PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
|---|---|---|
| Planning | 计划 | 35 |
| · Estimate | · 估计这个任务需要多少时间 | 35 |
| Development | 开发 | 448 |
| · Analysis | · 需求分析 (包括学习新技术) | 46 |
| · Design Spec | · 生成设计文档 | 27 |
| · Design Review | · 设计复审 | 8 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 |
| · Design | · 具体设计 | 34 |
| · Coding | · 具体编码 | 307 |
| · Code Review | · 代码复审 | 15 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 6 |
| Reporting | 报告 | 40 |
| · Test Repor | · 测试报告 | 19 |
| · Size Measurement | · 计算工作量 | 5 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 16 |
| · 合计 | 523 |
七、项目小结
7.1 成功经验
- 任务分工清晰:我们在项目开始时就明确了职责分工,保证了每个人都有明确任务,减少了重复劳动和沟通成本。
- 测试驱动开发思路:在开发过程中,我们不断补充测试用例,尤其是对分数、合法性、题目去重和判题统计等关键功能进行了重点验证。这样在后续修改时,能够更快发现问题并避免回归错误。
- 重视代码结构设计:模块化设计使程序各部分职责清晰,便于功能扩展与后期维护。
- 性能优化意识:在功能完成后,我们没有止步于“能用”,而是继续分析热点,主动进行性能优化,提高了程序的工程质量。
7.2 失败与不足
- 初期设计不够完善:开始时一些边界条件没有完全考虑到,例如分数转换、带分数表达式处理以及非法题目的过滤等,导致后期需要反复修改。
- 调试过程较耗时:一些问题并不是在代码中直接显眼,往往需要结合实际运行结果和测试日志进行定位,因此调试过程占用了较多时间。
- 代码优化时需要更谨慎:性能优化虽然必要,但如果缺乏分析,容易出现“改动很多但效果不明显”的情况。因此后续优化需要基于真实瓶颈而不是盲目调整。
7.3 结对感受
黄泽欣(主要负责设计思路和编写代码)
- 结对感受:这次结对项目让我感觉很有收获,团队协作的感觉特别强。我负责设计思路和编码,代代负责测试和漏洞排查,分工很明确,整体推进也比较顺畅。我们在开发过程中不断交流,问题解决得也比较快,感觉这次合作是比较高效的一次。
- 对方的闪光点:代代的最大闪光点在于测试思维和漏洞排查非常细致,总能在功能实现后第一时间发现潜在问题,很多时候都能提前避免错误的扩散。她的思维很严谨,不只是“能不能跑”,而是会不断追问“会不会有边界条件”“还有没有更隐蔽的问题”。这种态度让项目的稳定性提升了很多。
- 给对方的建议:希望代代继续保持这种严谨的测试习惯,同时也可以在设计讨论中更早参与进来。测试和漏洞排查很重要,但如果能在实现前更多地参与思路碰撞,会更容易减少返工,也会让项目开发更顺畅。
艾柯代·吐尔逊(主要负责测试和寻找漏洞)
- 结对感受:这次结对让我感受到泽欣的设计能力真的很强,她能把复杂需求拆成清晰的模块,把关键功能组织得很有层次,编码时也比较有条理。我们合作的时候,她总能先把整体框架搭起来,再慢慢补充细节,这样工作节奏比较稳,也让项目的结构更容易维护。
- 对方的闪光点:泽欣的闪光点在于设计思路和编码能力,不只是能把功能写出来,还会考虑结构、边界条件和后续维护问题。很多时候,她能快速找到一个比较合理的实现思路,并且把代码写得比较清晰,这对项目的整体质量非常重要。
- 给对方的建议:希望泽欣在后续项目中可以多和伙伴一起检查边界条件和异常情况。她的设计和编码能力很强,继续保持就很好,但如果再多一点和别人一起复盘的习惯,效率会更进一步。

浙公网安备 33010602011771号