结对项目

这个作业属于哪个课程 计科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

以下为整理后的优化前后性能分析对比图,可见在相同随机种子和相同题目规模下,优化后各热点函数的累计耗时明显下降,整体性能显著提升。
image

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

这些函数在程序中占用的 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 把分数转换成显示格式

它们之间的关系如图:
image

3.2.3 关键函数流程图

题目生成流程图(generate_questions)
image

判题流程图(grade)
image

四、代码说明

这个项目的核心是用“表达式树”来表示算式,并在生成题目和判题时反复计算和校验。最关键的代码集中在三处:入口函数、生成器、表达式解析与计算。

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.txtAnswers_test.txt 文件,确认文件正确生成。
8 test_parse_fraction_and_mixed_number 验证真分数和带分数可以被正确解析,例如 3/52’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 结对感受

黄泽欣(主要负责设计思路和编写代码)

  • 结对感受:这次结对项目让我感觉很有收获,团队协作的感觉特别强。我负责设计思路和编码,代代负责测试和漏洞排查,分工很明确,整体推进也比较顺畅。我们在开发过程中不断交流,问题解决得也比较快,感觉这次合作是比较高效的一次。
  • 对方的闪光点:代代的最大闪光点在于测试思维和漏洞排查非常细致,总能在功能实现后第一时间发现潜在问题,很多时候都能提前避免错误的扩散。她的思维很严谨,不只是“能不能跑”,而是会不断追问“会不会有边界条件”“还有没有更隐蔽的问题”。这种态度让项目的稳定性提升了很多。
  • 给对方的建议:希望代代继续保持这种严谨的测试习惯,同时也可以在设计讨论中更早参与进来。测试和漏洞排查很重要,但如果能在实现前更多地参与思路碰撞,会更容易减少返工,也会让项目开发更顺畅。

艾柯代·吐尔逊(主要负责测试和寻找漏洞)

  • 结对感受:这次结对让我感受到泽欣的设计能力真的很强,她能把复杂需求拆成清晰的模块,把关键功能组织得很有层次,编码时也比较有条理。我们合作的时候,她总能先把整体框架搭起来,再慢慢补充细节,这样工作节奏比较稳,也让项目的结构更容易维护。
  • 对方的闪光点:泽欣的闪光点在于设计思路和编码能力,不只是能把功能写出来,还会考虑结构、边界条件和后续维护问题。很多时候,她能快速找到一个比较合理的实现思路,并且把代码写得比较清晰,这对项目的整体质量非常重要。
  • 给对方的建议:希望泽欣在后续项目中可以多和伙伴一起检查边界条件和异常情况。她的设计和编码能力很强,继续保持就很好,但如果再多一点和别人一起复盘的习惯,效率会更进一步。
posted @ 2026-09-20 23:13  Lilimarlene  阅读(2)  评论(0)    收藏  举报