结对项目

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479
这个作业的目标 使用python完成小学四则运算程序,掌握结对编程的流程
GitHub链接 https://github.com/fanfanlilili/calculation

一、成员

3123004282 韦立凡
3123004532 廖杰

二、PSP表格

PSP2.1阶段 Personal Software Process Stages (个人软件过程阶段) 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 30
Estimate 估计这个任务需要多少开发时间 5 5
Development 开发 30 30
Analysis 需求分析(包括学习新技术) 60 60
Design Spec 生成设计文档 10 20
Design Review 设计复审 20 10
Coding Standard 代码规范(为目前的开发制定合适的规范) 20 10
Design 具体设计 60 90
Coding 具体编码 300 360
Code Review 代码复审 20 20
Test 测试(自我测试,修改代码,提交代码) 30 60
Reporting 报告 30 20
Test Repor 测试报告 20 30
Size Measurement 计算工作量 10 10
Postmortem & Process Improvement Plan 事后总结 10 10
合计 645 765

三、效能分析(耗费1.5小时)

通过在pycharm中安装line_profiler工具来进行性能分析,根据运行的结果,代码总运行时间是0.02s,整体性能不错,没有严重影响性能的瓶颈

主要瓶颈

1.耗时最大:generate_expression_ast函数耗时占总时间的一半,其中82行的generate_number的调用占约54%,95和96行的evaluate_ast各占约11%

1

改进思路:通过缓存以生成的数字,避免重复生成

_number_cache = {}
def get_cached_number(max_range):
key = max_range
if key not in _number_cache:
_number_cache[key] = generate_number(max_range)
return _number_cache[key]

2.to_custom_format函数中第23行f = Fraction(f)调用冗余

4

改进思路:删除冗余的fraction转换即可

def to_custom_format(f):
"""将 Fraction 对象或整数转换为指定的字符串格式。"""
if isinstance(f, int) or f.denominator == 1:
return str(int(f))

if f.numerator > f.denominator:
    integer_part = f.numerator // f.denominator
    remainder_numerator = f.numerator % f.denominator
    if remainder_numerator == 0:
        return str(integer_part)
    return f"{integer_part}'{remainder_numerator}/{f.denominator}"
else:
    return f"{f.numerator}/{f.denominator}"

改进结果

1.to_custom_format 函数性能提升明显(约40%)

11

2.generate_expression_ast 函数性能则提升约10%,得益于循环次数的减少(从212次减少到160次)

22

总体性能提升约7%,不过generate_expression_ast函数仍然是最大瓶颈,时间主要花在递归调用和表达式验证

四、设计实现过程

为了满足项目需求(生成题目、避免负数、保证除法为分数、去重等),我们采用了一种基于抽象语法树(Abstract Syntax Tree, AST)的核心设计思想。这种方法比直接拼接字符串更为强大和灵活,能够轻松地处理运算优先级、格式化输出和题目查重等复杂问题

1.代码组织结构

整个项目被组织在一个单独的 Python 文件 main.py 中,其内部结构按功能分为几个明确的部分:

一个核心类

ExpressionNode:这是我们数据结构的核心,用于构建表达式的抽象语法树。每个节点代表一个数字(叶节点)或一个运算符(内部节点)。内部节点有两个子节点(left 和 right),分别代表左操作数和右操作数。

一组功能函数

这些函数围绕 ExpressionNode 类工作,可以分为以下几类:

(1)数据格式化模块 (2个函数):

to_custom_format(): 将内部表示的 Fraction 对象(分数)转换为题目要求的字符串格式(如 2'1/3)。
from_custom_format(): 将字符串格式的数字解析回 Fraction 对象,用于答案批改。

(2)表达式生成模块 (3个函数):

generate_number(): 一个辅助函数,用于生成符合范围 r 的随机自然数或真分数。
generate_expression_ast(): 核心生成函数。它通过递归方式构建一个随机的AST。这是所有题目约束(如无负数、除法为分数)被强制执行的地方。
generate_problems(): 顶层控制函数,调用 generate_expression_ast() 来生成指定数量 -n 的题目,并通过 get_canonical_form() 来确保题目不重复。

(3)AST处理模块 (3个函数):

evaluate_ast(): 递归遍历AST,计算出表达式的精确答案。
format_ast_to_string(): 递归遍历AST,将其转换为符合人类阅读习惯的字符串,并根据运算符优先级自动处理括号。
get_canonical_form(): 去重关键函数。它为每个AST生成一个唯一的“范式”,通过对可交换运算符(+, ×)的操作数进行排序,确保 2 + 3 和 3 + 2 具有相同的范式。

(4)应用主逻辑模块 (2个函数):

check_answers(): 实现 -e 和 -a 参数的功能,读取文件并进行答案批改。
main(): 程序的入口点,负责解析命令行参数,并根据参数决定是调用 generate_problems() 还是 check_answers()。

2.关键函数工作流程

本节将详细描述两个核心函数 generate_expression_ast 和 check_answers 的工作流程。
image

generate_expression_ast() 工作流程

此函数是整个程序逻辑最复杂的函数,其工作流程如下:

(1)递归基准条件

检查剩余待生成的运算符数量。如果为0,或随机判定,则直接生成一个数字节点(ExpressionNode)并返回,终止当前分支的递归。

(2)递归主体

①随机选择一个运算符(+, -, ×, ÷)。
②将剩余的运算符数量随机分配给左、右两个子树。
③进入一个无限循环 (while True),以确保生成的子表达式满足所有约束条件:
④递归调用 generate_expression_ast() 来生成左、右子节点。
⑤调用 evaluate_ast() 计算出左右子表达式的数值。
⑥执行约束检查:如果运算符是 -,检查是否 左数值 < 右数值。如果是,则放弃本次生成的子树,继续循环以重新生成;如果运算符是 ÷,检查是否 右数值 == 0(避免除零)或 左数值 / 右数值 的结果为整数。如果是,则放弃并继续循环。
⑦成功退出: 如果所有约束检查都通过,则用当前运算符和生成的左右子节点创建一个新的 ExpressionNode,跳出循环并将其返回。
这个流程确保了任何最终生成的表达式树都是完全符合题目要求的。

(3)check_answers() 工作流程

此函数负责执行判卷任务,其逻辑是一个线性的、按部就班的处理流程:
image
①初始化:
创建两个空列表:correct_indices 用于存放正确题目的编号,wrong_indices 用于存放错误题目的编号。
②文件读取:
尝试读取 -e 参数指定的习题文件和 -a 参数指定的答案文件的所有行。
如果任何一个文件未找到,则捕获异常,打印错误信息并终止函数。
③逐行批改:
进入一个循环,遍历读取到的每一道习题。
在循环内部,对每一行(即每一道题)执行以下操作:
a. 数据提取: 从习题行中解析出题目表达式字符串,并从对应的答案行中解析出用户作答的答案字符串。
b. 计算正确答案: 调用一个安全的求值函数 (safe_eval_wrapper),将题目表达式字符串计算出其精确的 Fraction 结果。
c. 解析用户答案: 调用 from_custom_format() 函数,将用户作答的字符串(例如 3'1/4)也转换为 Fraction 对象。
d. 比较与记录: 比较计算出的正确答案和用户答案。

  • 如果两者相等,则将当前题号添加到 correct_indices 列表。
  • 如果不相等,或在处理该行的任何步骤中发生错误,则将当前题号添加到 wrong_indices 列表。
    ④生成报告:
    循环结束后,打开 Grade.txt 文件进行写入。
    根据 correct_indices 和 wrong_indices 两个列表的内容,格式化并写入最终的统计结果(例如 Correct: 5 (1, 3, 5, 7, 9))。

五、代码说明

以下是项目中几个最关键的代码片段及其设计思路。

1.表达式生成与约束检查

这是在 generate_expression_ast 函数中实现的,while True 循环是确保所有约束都被满足的关键。
这是在 generate_expression_ast 函数中实现的,while True 循环是确保所有约束都被满足的关键。
def generate_expression_ast(max_range, ops_remaining):
op = random.choice(OPERATORS)
# ... 分配子节点运算符数量 ...

while True: # 循环直到生成一个有效的表达式
    left_child = generate_expression_ast(max_range, left_ops)
    right_child = generate_expression_ast(max_range, right_ops)
    
    # 计算出子表达式的值,用于检查约束
    left_val = evaluate_ast(left_child)
    right_val = evaluate_ast(right_child)
    
    # 约束1:减法结果不能为负数
    if op == '-' and left_val < right_val:
        continue # 不满足条件,重新生成左右子树
    
    # 约束 2:除法不能除以0,且结果必须是真分数
    if op == '÷':
        if right_val == 0:
            continue # 避免除零
        result = left_val / right_val
        if result.denominator == 1:
            continue # 结果是整数,不符合要求,重新生成
    
    # 如果所有约束都通过,则创建节点并跳出循环
    return ExpressionNode(op, left_child, right_child)

思路与注释

递归构建: 通过递归调用,程序可以构建出任意深度的表达式树(本项目限制在3个运算符内)。
生成后验证 (Generate-and-Test): 我们采用“先生成,再验证”的模式。while True 循环确保了只有通过所有约束检查的表达式片段才能被接受。这种方式虽然可能有性能开销(少数情况下会多次重试),但逻辑清晰,易于实现和扩展新的约束。

2.通过“范式”实现题目去重

get_canonical_form 函数是去重的核心。它将一个表达式AST转换成一个唯一的、可哈希的元组(Tuple)。
def get_canonical_form(node):
"""创建一个唯一的、排序过的AST表示,用于检测重复题目。"""
if node.is_leaf():
# 叶节点(数字)的范式就是其分子分母元组
return (node.value.numerator, node.value.denominator)

op = node.value
left_canonical = get_canonical_form(node.left)
right_canonical = get_canonical_form(node.right)

# 核心逻辑:对于可交换运算符,对子节点的范式进行排序
if op in ['+', '×']:
    # 使用 key=str 确保不同类型(元组和字符串)之间可以稳定排序
    return (op, tuple(sorted((left_canonical, right_canonical), key=str)))
else: # 对于-和÷,顺序是固定的
    return (op, left_canonical, right_canonical)

六、测试运行

我们设计了12个测试用例,覆盖了所有项目需求,以确保程序的正确性。

# 测试目的 命令 预期结果 如何确认正确性
1 基本生成功能 python main.py -n 10 -r 10 成功生成 Exercises.txt 和 Answers.txt 两个文件,每个文件包含10道题目/答案,程序正常退出。 检查文件是否存在且内容行数正确,验证了 -n 和 -r 参数的基本功能。
2 负数约束 python main.py -n 1000 -r 20 生成的1000道题目中,任何形如 a - b 的表达式,其计算结果都不应为负数。 手动抽查或编写脚本检查 Exercises.txt,确认所有减法 a - b 均满足 a >= b。验证了减法约束。
3 除法约束 python main.py -n 1000 -r 20 生成的1000道题目中,任何形如 a ÷ b 的表达式,其计算结果都不能是整数。 检查 Answers.txt 中所有题目的答案,确认没有任何一道题的最终答案或中间除法步骤结果为整数(除非原始数字是整数)。验证了除法约束。
4 运算符数量约束 python main.py -n 500 -r 15 生成的题目中,每道题的运算符(+, -, ×, ÷)数量都不超过3个。 抽查 Exercises.txt,目视检查确认运算符数量在1到3个之间。验证了题目复杂度约束。
5 加法去重 python main.py -n 10000 -r 5 生成一万道题,由于数值范围很小,必然会产生大量等价题目。程序应能过滤掉重复项,最终成功生成一万道不重复的题目。 运行时间会较长,但最终会成功。如果 2+3 和 3+2 被视为不同,程序会很快完成;反之,则需要更多时间寻找不重复的组合。这验证了去重逻辑(特别是加法交换律)在起作用。
6 乘法去重 python main.py -n 10000 -r 5 同上 与测试5同理,验证了乘法交换律的去重。
7 复杂结构去重 python main.py -n 10000 -r 6 表达式 3 × (1 + 2) 和 (2 + 1) × 3 应被视为重复。 在大批量生成中,这两者不会同时出现。通过 get_canonical_form 的逻辑可以推断其正确性,因为子表达式 (1+2) 和 (2+1) 的范式相同。
8 文件格式正确性 python main.py -n 5 -r 10 Exercises.txt 和 Answers.txt 的格式应为 题号. 题目/答案,分数应以 a'b/c 或 b/c 的形式展示。 打开生成的文件,检查格式是否完全符合要求。验证了自定义格式化函数的正确性。
9 答案批改(全对) python main.py -e Exercises.txt -a Answers.txt 首先生成一组题目和答案。然后用生成的答案文件去批改。Grade.txt 应显示 Correct: N (1, 2, ..., N),Wrong: 0 ()。 验证了 check_answers 函数在答案完全正确时的逻辑。
10 答案批改(部分错误) 手动修改 Answers.txt 中的几行,例如将 3/5 改为 4/5。然后运行 python main.py -e Exercises.txt -a Answers.txt。 Grade.txt 应准确地报告被修改的题号为 Wrong,其余为 Correct。 验证了 check_answers 函数识别错误答案的能力。
11 参数错误处理 python main.py 程序应报错,并打印帮助信息,提示用户如何正确使用参数。 验证了 main 函数中的参数校验逻辑。
12 范围参数错误处理 python main.py -n 10 -r 1 程序应报错,提示 -r 参数必须大于等于2。 验证了对 -r 参数的边界条件检查,因为小于2的范围无法生成真分数。

(1)基本生成功能

image
成功生成 Exercises.txt 和 Answers.txt 两个文件,每个文件包含10道题目/答案,程序正常退出
image
image

(2)负数约束

image
生成的1000道题目中,任何形如 a - b 的表达式,其计算结果都不应为负数。

如图截取几个例子符合约束:

image
image

(3)除法约束

生成的1000道题目中,任何形如 a ÷ b 的表达式,其计算结果都不能是整数。
如图截取几个例子 符合约束:
image
image

(4)运算符数量约束

生成的题目中,每道题的运算符(+, -, ×, ÷)数量都不超过3个。
如图随机抽取符合例子:
image

(5)(6)(7)成功生成一万个例子并且符合:加法去重、乘法去重、复杂结构去重

一万个例子:

image
image
image
如果 2+3 和 3+2 被视为不同,程序会很快完成;反之,则需要更多时间寻找不重复的组合。这验证了去重逻辑(特别是加法交换律)在起作用。与测试5同理,验证了乘法交换律的去重。在大批量生成中,这两者不会同时出现。通过 get_canonical_form 的逻辑可以推断其正确性,因为子表达式 (1+2) 和 (2+1) 的范式相同。
均没有重复项,均符合约束。

(8)文件格式正确性

Exercises.txt 和 Answers.txt 的格式应为 题号. 题目/答案,分数应以 a'b/c 或 b/c 的形式展示。
image
image

(9)答案批改(全对)

首先生成一组题目和答案。然后用生成的答案文件去批改。Grade.txt 应显示 Correct: N (1, 2, ..., N),Wrong: 0 ()。
用1的例子来做显示如下:
image

(10)答案批改(部分错误)

手动修改前三题的答案:1/8改为1 31改为3 8改为81
image
image
结果如下:
image
验证正确

(11)参数错误处理

输入python main.py程序应报错,并打印帮助信息,提示用户如何正确使用参数。
image

(12)范围参数错误处理

程序应报错,提示 -r 参数必须大于等于2。
image

七、项目小结

1.成功之处

(1)更优的设计方案:项目初期,我们在“如何表示和生成表达式”有分歧。经过讨论,我们最终放弃了简单的字符串处理,共同选择了抽象语法树(AST)作为核心数据结构。事实证明,这个决定是项目的最大亮点,它解决了处理运算优先级、添加括号、题目查重等难题。
(2)更高的代码质量:一人编码、一人审查的模式让很多低级错误(如拼写错误、逻辑疏忽)在萌芽阶段就被发现,明显提升代码健壮性
(3)知识共享与共同成长:廖杰在整体架构和算法设计上更具条理,而韦立凡在代码实现细节和调试上嗅觉敏锐。通过结对,我们互相学到了对方的优点。整个项目下来,我们都感觉自己的能力得到了补强。
(4)攻坚克难的效率:项目中最难的部分是“题目去重”。我们一起设计了“表达式范式”get_canonical_form()函数,通过对可交换运算符的子节点进行排序,完美解决了这个问题,此过程也让我们感受到了结对编程的效率。

2.教训

(1)编程角色的挑战:有时候,当编程者思维活跃时,辅助者很难插上话,容易变成一个被动的“代码检视员”。反之,当编程者卡壳时,辅助如果不能及时提供有效思路,场面会一度陷入尴尬。我们仍需学习如何动态、高效地切换角色。
(2)时间管理的冲突:两个人要找到大段的、共同的、高效的工作时间并不容易,这也提醒我们,好的结对编程需要有严格的时间规划和承诺。

3.结对感受

廖杰:韦立凡更注重代码细节,几次在我陷入僵局时,他帮助我定位到隐藏极深的 bug。他提出的 while True 循环来确保约束满足的“生成-测试”模型,虽然简单粗暴,但异常有效。和他搭档,总能感受到一种积极解决问题的能量。
韦立凡:项目初期,是廖杰坚持要使用抽象语法树,为整个项目的成功奠定了坚实的基础。我对约束条件不敏感,是他用清晰的流程图和逻辑描述将问题梳理得井井有条,他的严谨和远见为我节约了大量时间。

4.总结

总而言之,这次“四则运算生成器”的结对编程经历远超预期。从最初的需求分析到最终的代码实现,它不仅仅是两个人合作完成了一个项目,更是一次关于沟通、信任和团队协作的深度实践,让我们明白清晰的分工和灵活的角色切换是效率的保障。虽然过程会有错误和争论,但最终我们仍然做出了我们都满意的作品,它所带来的成就感是无可比拟的。

posted @ 2025-10-21 21:25  fanfanlilili  阅读(8)  评论(0)    收藏  举报