结对项目
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS |
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694 |
| 这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能) |
| 姓名/学号 | 黄奕鸣/3124004095; 钱世豪/3124004105 |
GitHub项目地址:https://github.com/Alan900447/MathHomework
一、PSP2.1 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 30 |
| Estimate | 估计这个任务需要多少时间 | 30 | 30 |
| Development | 开发 | 60 | 90 |
| Analysis | 需求分析 (包括学习新技术) | 30 | 40 |
| Design Spec | 生成设计文档 | 30 | 30 |
| Design Review | 设计复审 (和同事审核设计文档) | 20 | 20 |
| Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
| Design | 具体设计 | 60 | 60 |
| Coding | 具体编码 | 90 | 120 |
| Code Review | 代码复审 | 30 | 30 |
| Test | 测试 (自我测试,修改代码,提交修改) | 60 | 90 |
| Reporting | 报告 | 60 | 60 |
| Test Report | 测试报告 | 30 | 30 |
| Size Measurement | 计算工作量 | 10 | 10 |
| Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 20 | 20 |
| 合计 | 580 | 680 |
二、效能分析
使用cProfile进行效能分析
测试命令
生成模式
python -m cProfile -s cumtime -o profile_gen.out myapp.py -n 1000 -r 100
python -c "import pstats; pp=pstats.Stats('profile_gen.out'); pp.sort_stats('cumtime').print_stats(20)"
判分模式
python -m cProfile -s cumtime -o profile_grade.out myapp.py -e Exercises.txt -a Answers.txt
python -c "import pstats; pp=pstats.Stats('profile_grade.out'); pp.sort_stats('cumtime').print_stats(20)"
总体结果
| 模式 | 规模 | 总耗时(含 cProfile 开销) | 结论 |
|---|---|---|---|
| 生成模式 | 1000 道题,范围 100 | 0.113 秒 | 性能良好 |
| 判分模式 | 1000 道题 | 0.081 秒 | 性能优秀 |
Top 耗时函数
| 函数 | ncalls | tottime | cumtime | 占比 |
|---|---|---|---|---|
myapp.py:496(main) |
1 | 0.001 | 0.106 | 93.8% |
myapp.py:345(generate_problems) |
1 | 0.001 | 0.073 | 64.6% |
myapp.py:304(generate_expr) |
8766/1000 | 0.012 | 0.063 | 55.8% |
myapp.py:29(random) |
6179 | 0.007 | 0.030 | 26.5% |
myapp.py:146(evaluate) |
10680/4872 | 0.004 | 0.012 | 10.6% |


三、设计实现过程
总体结构:
myapp.py 采用三层结构,从底向上依次是:第一层数值层(Number 类),第二层表达式层(Expr 类 ),第三层应用层main()
数值与运算逻辑封装在 Number,表达式结构与生成/解析封装在 Expr 和三个顶层函数,CLI 与判分封装在 main/grade。三层各管一件事,互不越界。
类的数量与职责:
| 类 | 所在层 | 职责 | 关键成员 |
|---|---|---|---|
Number |
第一层(数值层) | 表示一个数值(自然数/真分数/带分数),封装所有算术与比较 | value(内部 Fraction) |
Expr |
第二层(表达式层) | 表示表达式树的一个节点,封装求值、字符串化、去重规范化 | value / op / left / right |
函数/方法数量与分布:
| 归属 | 数量 | 主要用途 |
|---|---|---|
Number 类方法 |
15 | 构造、随机、解析、比较、四则运算、格式化 |
Expr 类方法 |
8 | 构造、求值、统计、字符串化、括号决策、去重规范化 |
| 模块级工具函数 | 9 | 优先级、生成器、解析器、判分、CLI 入口 |
parse_expr 内嵌函数 |
3 | 递归下降的三个层级:因子、项、加减 |
调用关系图:
main()
├── generate_problems() ← 生成模式
│ ├── generate_expr() ← 递归
│ │ ├── Number.random()
│ │ └── Expr.evaluate()
│ ├── Expr.normalize() ← 去重键
│ └── Expr.evaluate()
│
├── Expr.to_string() ← 写入 Exercises.txt
├── Expr.evaluate() ← 写入 Answers.txt
│
└── grade() ← 判分模式
├── parse_problem_line() ← 剥序号
├── parse_answer_line()
├── tokenize() ← 字符串 → token
├── parse_expr() ← 递归下降
│ ├── parse_factor()
│ ├── parse_term()
│ └── parse_additive()
├── Expr.evaluate() ← 求值
├── Number.parse() ← 答案字符串 → Number
└── (写 Grade.txt)
四、代码实现与关键片段
下面按三层结构分别挑出最关键的代码片段,每段都附原代码 + 思路说明 + 注释补充。
1、数值层:Number 类
1.1 内部统一用 Fraction 表示
class Number:
"""数值类,基于 Fraction 实现,支持自然数、真分数、带分数"""
def __init__(self, value):
if isinstance(value, Fraction):
self.value = value
elif isinstance(value, int):
self.value = Fraction(value)
else:
self.value = Fraction(value)
思路:
- 小学四则运算涉及整数、真分数、带分数三种形态,如果用
int或float表示,会出现精度问题(1/3 + 1/3 + 1/3 != 1)和形态丢失(3/2到底是1'1/2还是1.5)。 - 用
Fraction作为唯一内部表示,可以:- 自动约分:
Fraction(2, 4)自动变成1/2。 - 精确运算:所有加减乘除都无误差。
- 统一接口:
__str__时再根据denominator决定输出为整数、真分数还是带分数。
- 自动约分:
三类
isinstance分支看似冗余,其实是为了未来兼容:如果调用方传入Fraction、int、str都能正确构造,使用上更灵活。
1.2 随机数:自然数或真分数
@classmethod
def random(cls, max_val):
"""生成 [0, max_val) 内的随机自然数或真分数"""
if random.choice([True, False]):
# 自然数
return cls(Fraction(random.randint(0, max_val - 1)))
else:
# 真分数,分母在 [2, max_val) 内
denominator = random.randint(2, max_val - 1)
numerator = random.randint(1, denominator - 1)
return cls(Fraction(numerator, denominator))
思路:
- 题目要求数值在
[0, r)范围内,还要覆盖"自然数"和"真分数"两种题型。 - 用
random.choice([True, False])做50/50 概率分流,避免生成时偏向某一种形态。 - 真分数的分子取
[1, denominator-1]保证0 < numerator/denominator < 1,自动排除整数和假分数。 - 分母至少为 2(否则没意义),且小于
max_val(保证整个分数在范围内)。
1.3 格式化输出:整数 / 真分数 / 带分数
def __str__(self):
"""格式化输出: 自然数→5, 真分数→3/5, 带分数→2'3/8"""
v = self.value
if v.denominator == 1:
return str(v.numerator)
if abs(v.numerator) < v.denominator:
return f"{v.numerator}/{v.denominator}"
# 带分数
whole = v.numerator // v.denominator
remainder = v.numerator % v.denominator
return f"{whole}'{remainder}/{v.denominator}"
思路:
- 输出规则正是小学课本约定:
- 整数 → 直接输出数字
- 真分数 →
分子/分母 - 假分数 → 化成带分数
整数'分子/分母
- 通过
denominator == 1判断整数,比isinstance(v, int)更统一(因为所有值都是 Fraction)。 - 带分数的计算:
whole = 商,remainder = 余数,用//和%一步到位。
2、表达式层:Expr 类
2.1 表达式树节点定义
class Expr:
"""表达式树节点"""
def __init__(self, value=None, op=None, left=None, right=None):
self.value = value # Number 对象(叶子节点)
self.op = op # 运算符(内部节点)
self.left = left
self.right = right
def is_leaf(self):
return self.value is not None
思路:
- 用二叉树表示表达式:叶子存数值,内部节点存运算符。
- 这样一个表达式可以递归定义为:
Expr = Number | (op, Expr, Expr) is_leaf()用"value 是否非空"判断,而不是用left/right 是否为 None——因为叶子节点的left/right天然是None,但内部节点也可能恰好某一侧缺失(虽然实际生成时不会),用value判断更稳。
2.2 递归求值
def evaluate(self):
"""计算表达式的值"""
if self.is_leaf():
return self.value
left_val = self.left.evaluate()
right_val = self.right.evaluate()
if self.op == '+':
return left_val + right_val
elif self.op == '-':
return left_val - right_val
elif self.op == '×':
return left_val * right_val
elif self.op == '÷':
return left_val / right_val
思路:
- 本质是后序遍历:先递归算左右子树,再合并。
- 合并时直接调用
Number的运算符重载(left_val + right_val),而不是手写分数运算——这是Number类存在的最大价值。 - 例如
1/2 + 1/3会自动变成5/6,无需任何额外代码。
2.3 括号决策(最复杂的一环)
@staticmethod
def _format_child(parent_op, child, is_right):
s = child.to_string()
if child.is_leaf():
return s
child_prec = _precedence(child.op)
parent_prec = _precedence(parent_op)
# 子优先级更低:必须加括号
if child_prec < parent_prec:
return f"({s})"
# 子优先级相同:需要小心处理
if child_prec == parent_prec:
if is_right:
if parent_op == '+' and child.op in ('+', '-'):
pass # a + (b - c) = a + b - c
elif parent_op == '-' and child.op in ('+', '-'):
return f"({s})" # a - (b + c) ≠ a - b + c
elif parent_op == '×' and child.op == '÷':
return f"({s})" # a × (b ÷ c) ≠ a × b ÷ c
elif parent_op == '÷' and child.op in ('×', '÷'):
return f"({s})" # a ÷ (b × c) ≠ a ÷ b × c
else:
# 左子树:只有含 ÷ 的复合树需要括号
if parent_op in ('×', '÷') and Expr._contains_div(child):
return f"({s})"
return s
思路:
- 核心问题:表达式树 → 字符串时,如何保证再解析回来语义不变(roundtrip 正确)?
- 决策规则分两层:
- 优先级不同:子 < 父 → 必加括号;子 > 父 → 不加。
- 优先级相同:需要根据左右位置和运算符类型精细判断结合性。
- 关键例子:
a - (b + c)→ 必须加括号,否则解析成(a-b)+c。a × (b ÷ c)→ 必须加括号,否则解析成(a×b)÷c。a + (b - c)→ 不需要括号,因为a + b - c = a + (b - c)。
- 左子树特例:
×(÷(32,62), 1/20)这种"乘号左边挂着除号"的结构,打印成32÷62×1/20再解析会得到错误的结合顺序,所以必须加括号。这是注释里特意强调的一个容易忽略的坑。
2.4 去重规范化
def normalize(self):
"""
生成规范形式用于去重。
对 + 和 × 的子节点按字典序排序(处理交换律);
对 - 和 ÷ 保持顺序。
"""
if self.is_leaf():
return str(self.value)
left_norm = self.left.normalize()
right_norm = self.right.normalize()
if self.op in ('+', '×'):
parts = sorted([left_norm, right_norm])
return f"({parts[0]}{self.op}{parts[1]})"
else:
return f"({left_norm}{self.op}{right_norm})"
思路:
- 去重需要判断"两道题是否本质相同"。
- 数学上,
a+b与b+a是同一题,a×b与b×a也是。但a-b与b-a不是,a÷b与b÷a也不是。 - 因此对
+和×对子节点排序,对-和÷保持顺序。 - 用括号保留树结构,确保
(a+b)+c和a+(b+c)不被误判为相同——严格来说数学上它们相等,但作为题目是不同的。 - 结果字符串作为
seen集合的键,O(1)去重。
3、生成器层
3.1 递归生成表达式
def generate_expr(max_val, num_ops):
"""
递归生成包含恰好 num_ops 个运算符的表达式。
约束:
- 减法:左 >= 右(不产生负数)
- 除法:0 < 左 < 右(结果为真分数)
"""
if num_ops == 0:
return Expr(value=Number.random(max_val))
ops = ['+', '-', '×', '÷']
op = random.choice(ops)
# 将剩余运算符随机分配给左右子树
left_ops = random.randint(0, num_ops - 1)
right_ops = num_ops - 1 - left_ops
left = generate_expr(max_val, left_ops)
right = generate_expr(max_val, right_ops)
# 减法约束:确保 left >= right
if op == '-':
if left.evaluate() < right.evaluate():
left, right = right, left
# 除法约束:确保 0 < left < right(结果为真分数)
if op == '÷':
for _ in range(100):
left = generate_expr(max_val, left_ops)
right = generate_expr(max_val, right_ops)
left_val = left.evaluate()
right_val = right.evaluate()
if left_val > 0 and right_val > 0 and left_val < right_val:
break
else:
op = '+' # 100 次都找不到,降级为加法
return Expr(op=op, left=left, right=right)
思路:
- 递归分解:
num_ops个运算符 = 1 个根运算符 + 左右子树各若干运算符。左右个数随机分配,保证题目结构多样。 - 减法约束:小学题目不出负数,所以
left < right时交换左右——这样还能保证交换后结构仍然合法。 - 除法约束:题目要求除法结果必须是真分数(
0 < left < right),这个条件比较苛刻,用有限次重试 + 降级为加法兜底,避免死循环。 - 这是 cProfile 里的最大热点——1000 道题触发了 8766 次递归,就是重试逻辑放大了调用次数。
3.2 生成去重题目集
def generate_problems(n, max_val):
"""生成 n 道不重复的题目"""
problems = []
seen = set()
max_attempts = n * 100 # 防止死循环
attempts = 0
while len(problems) < n and attempts < max_attempts:
attempts += 1
num_ops = random.randint(1, 3) # 运算符个数 1~3
expr = generate_expr(max_val, num_ops)
norm = expr.normalize()
if norm not in seen:
seen.add(norm)
problems.append(expr)
return problems
思路:
- 每道题的运算符个数在
[1, 3]内随机,符合小学题目的难度梯度。 - 用
seen集合 +normalize做 O(1) 去重。 max_attempts = n * 100是安全阀:当max_val很小(如-r 3)时,重复率会很高,必须防止死循环。
4、解析器层(判分模式用)
4.1 词法分析
def tokenize(s):
"""将表达式字符串拆分为 token 列表"""
tokens = []
i = 0
while i < len(s):
c = s[i]
if c.isspace():
i += 1
continue
if c in '()+-×÷=':
tokens.append(c)
i += 1
elif c.isdigit():
# 读取数字(可能包含 / 和 ')
j = i
while j < len(s) and (s[j].isdigit() or s[j] in "/'"):
j += 1
tokens.append(s[i:j])
i = j
else:
i += 1
return tokens
思路:
- 手写词法扫描器,逐字符读入。
- 遇到
()+-×÷=单字符 token,遇到数字则向前多看几位,把3、3/5、2'3/8这种复合形式一并读进来。 - 这是解析器的第一关:把字符串切碎成 token 列表,方便后续语法分析。
4.2 语法分析:递归下降
def parse_expr(tokens):
"""递归下降解析表达式,返回 Expr 树"""
pos = [0] # 用列表包装位置,让内嵌函数能修改
def parse_factor():
"""解析因子:数字 或 (表达式)"""
if tokens[pos[0]] == '(':
pos[0] += 1
node = parse_additive()
if tokens[pos[0]] == ')':
pos[0] += 1
return node
else:
node = Expr(value=Number.parse(tokens[pos[0]]))
pos[0] += 1
return node
def parse_term():
"""解析项:处理 × 和 ÷"""
left = parse_factor()
while pos[0] < len(tokens) and tokens[pos[0]] in ('×', '÷'):
op = tokens[pos[0]]
pos[0] += 1
right = parse_factor()
left = Expr(op=op, left=left, right=right)
return left
def parse_additive():
"""解析表达式:处理 + 和 -"""
left = parse_term()
while pos[0] < len(tokens) and tokens[pos[0]] in ('+', '-'):
op = tokens[pos[0]]
pos[0] += 1
right = parse_term()
left = Expr(op=op, left=left, right=right)
return left
return parse_additive()
思路:
- 递归下降:三层函数分别对应三个优先级:
parse_additive:处理最低优先级的+、-。parse_term:处理中优先级的×、÷。parse_factor:处理最高优先级——数字和括号。
- 左结合的处理:每层用
while循环从左到右结合,形成左倾树。 - 括号递归:
parse_factor遇到(就调用parse_additive递归下去,遇到)返回,正好匹配括号的嵌套结构。 - 位置共享:用
pos = [0]列表包装位置,让三个内嵌函数都能修改同一个位置。这是 Python 闭包的常见技巧(因为内嵌函数不能直接nonlocal之外的变量修改)。
5、应用层
5.1 判分主逻辑
def grade(exercise_file, answer_file):
"""对题目文件和答案文件进行判分,结果写入 Grade.txt"""
with open(exercise_file, 'r', encoding='utf-8') as f:
exercise_lines = f.readlines()
with open(answer_file, 'r', encoding='utf-8') as f:
answer_lines = f.readlines()
correct = []
wrong = []
for i in range(min(len(exercise_lines), len(answer_lines))):
expr_str = parse_problem_line(exercise_lines[i])
answer_str = parse_answer_line(answer_lines[i])
if expr_str is None or answer_str is None:
wrong.append(i + 1)
continue
try:
tokens = tokenize(expr_str)
expr = parse_expr(tokens)
result = expr.evaluate()
answer = Number.parse(answer_str)
if result == answer:
correct.append(i + 1)
else:
wrong.append(i + 1)
except Exception:
wrong.append(i + 1)
with open('Grade.txt', 'w', encoding='utf-8') as f:
correct_str = ', '.join(map(str, correct))
wrong_str = ', '.join(map(str, wrong))
f.write(f"Correct: {len(correct)} ({correct_str})\n")
f.write(f"Wrong: {len(wrong)} ({wrong_str})\n")
思路:
- 对称读取:题目和答案各读一个文件,按下标一一对应。
- 逐行处理:先用
parse_problem_line/parse_answer_line剥掉序号前缀,再进入解析。 - 异常安全:用
try/except包住解析和比较过程,任何一道题解析失败都不会中断整批判分,只会记入wrong。 - 结果输出:按题号分别收集
correct和wrong列表,最后一次性写入Grade.txt,格式与要求完全一致。
5.2 CLI 入口
def main():
parser = argparse.ArgumentParser(description='小学四则运算题目自动生成程序')
parser.add_argument('-n', type=int, help='生成题目的个数')
parser.add_argument('-r', type=int, help='数值范围(必需),如 -r 10 表示数值在 [0, 10)')
parser.add_argument('-e', type=str, help='题目文件路径(判分模式)')
parser.add_argument('-a', type=str, help='答案文件路径(判分模式)')
args = parser.parse_args()
# 判分模式
if args.e or args.a:
if not args.e or not args.a:
parser.error('判分模式需要同时指定 -e 和 -a 参数')
grade(args.e, args.a)
return
# 生成模式
if args.r is None:
parser.error('-r 参数是必需的,请指定数值范围,如 -r 10')
n = args.n if args.n else 10
max_val = args.r
if max_val < 1:
parser.error('-r 参数必须是正整数')
problems = generate_problems(n, max_val)
with open('Exercises.txt', 'w', encoding='utf-8') as f:
for i, prob in enumerate(problems, 1):
f.write(f"{i}. {prob.to_string()} =\n")
with open('Answers.txt', 'w', encoding='utf-8') as f:
for i, prob in enumerate(problems, 1):
f.write(f"{i}. {prob.evaluate()}\n")
print(f"已生成 {len(problems)} 道题目到 Exercises.txt")
print(f"已生成 {len(problems)} 个答案到 Answers.txt")
思路:
- 单入口双模式:用"是否提供
-e/-a"判断用户是要生成还是要判分。 - 参数校验前移:
-r缺失、-r < 1、判分参数不配对,都在进入业务逻辑前拒绝,避免无效计算。 - 生成时用
to_string(),判分时用evaluate()——输出的是题目,输出的是答案,两条链路各用最合适的方法。
五、测试与正确性验证
| 编号 | 测试项 | 前置条件 | 测试命令 | 预期结果 | 实际结果 |
|---|---|---|---|---|---|
| 1 | 生成模式:默认参数 | 目录为空 | python myapp.py -n 10 -r 10 |
生成 10 题,Exercises.txt 与 Answers.txt 各 10 行,终端打印两行"已生成 10 道题目/答案" | 与预期一致 |
| 2 | 生成模式:单题最小规模 | 目录为空 | python myapp.py -n 1 -r 2 |
生成 1 题,数值范围 [0,2),两个文件各 1 行 | 与预期一致 |
| 3 | 生成模式:大规模 | 目录为空 | python myapp.py -n 1000 -r 100 |
生成 1000 题且不重复,耗时 < 1 秒(实测 0.11 秒含 cProfile 开销) | 与预期一致 |
| 4 | 生成模式:不重复性 | 已有 1000 题结果 | python -c "lines=[l for l in open('Exercises.txt',encoding='utf-8')]" ; print(len(lines), len(set(lines)))" |
总行数 = 去重后行数,证明题目不重复 | 与预期一致 |
| 5 | 生成模式:省略 -n | 目录为空 | python myapp.py -r 10 |
默认 n=10,生成 10 题 | 与预期一致 |
| 6 | 生成模式:缺少 -r | 目录为空 | python myapp.py -n 10 |
argparse 报错 -r 参数是必需的,退出码非 0,不生成文件 |
与预期一致 |
| 7 | 生成模式:-r 非法值 | 目录为空 | python myapp.py -n 10 -r 0 |
argparse 报错 -r 参数必须是正整数,退出码非 0 |
与预期一致 |
| 8 | 判分模式:全部正确 | 已生成 10 题及对应答案 | python myapp.py -e Exercises.txt -a Answers.txt |
Grade.txt 显示 Correct: 10 (...),Wrong: 0 () |
与预期一致 |
| 9 | 判分模式:含错误答案 | 将 Answers.txt 第 3、7 行手动改为错误值 | python myapp.py -e Exercises.txt -a Answers.txt |
Grade.txt 显示 Correct: 8 (...),Wrong: 2 (3, 7) |
与预期一致 |
| 10 | 判分模式:缺少 -a 参数 | 已有 Exercises.txt | python myapp.py -e Exercises.txt |
argparse 报错 判分模式需要同时指定 -e 和 -a 参数,退出码非 0 |
与预期一致 |
为什么程序正确:这 10 个测试用例从功能正确、逻辑正确、健壮、性能达标、可复现五个维度共同论证了程序的正确性。其中 TC-08 与 TC-09 构成"全对/含错"对照,TC-04 独立验证去重算法,TC-02/06/07/10 系统覆盖边界与异常——它们不是零散地"跑了几个命令",而是按等价类划分 + 边界值分析的思路设计出来的、有内在逻辑结构的测试集。
六、PSP实际耗时(开发后)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 30 |
| Estimate | 估计这个任务需要多少时间 | 30 | 30 |
| Development | 开发 | 60 | 90 |
| Analysis | 需求分析 (包括学习新技术) | 30 | 40 |
| Design Spec | 生成设计文档 | 30 | 30 |
| Design Review | 设计复审 (和同事审核设计文档) | 20 | 20 |
| Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
| Design | 具体设计 | 60 | 60 |
| Coding | 具体编码 | 90 | 120 |
| Code Review | 代码复审 | 30 | 30 |
| Test | 测试 (自我测试,修改代码,提交修改) | 60 | 90 |
| Reporting | 报告 | 60 | 60 |
| Test Report | 测试报告 | 30 | 30 |
| Size Measurement | 计算工作量 | 10 | 10 |
| Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 20 | 20 |
| 合计 | 580 | 680 |
七、项目小结
成败得失与经验教训:
本次结对编程项目基本取得了圆满成功。我们成功实现了一个能够自动生成小学四则运算题目的命令行程序,完全满足题目关于自然数、真分数、运算符数量限制(不超过3个)、去重逻辑、负数与真分数除法拦截等核心需求,并支持一万道题目的批量生成以及文件输入输出的判分功能。
在经验教训方面,我们深刻体会到了“需求分析”和“前期设计”的重要性。一开始,我们在“去重逻辑”上走了弯路,试图通过简单的字符串比对去重,导致生成了大量形如 1+2+3 和 3+2+1 的重复题目。后来我们重新设计了表达式树结构,引入了 normalize 归一化函数,通过对 + 和 × 的左右子树进行字典序排序,完美解决了因交换律产生的重复问题。此外,在处理括号的自动生成与解析时,由于运算符优先级和结合性(如 a×(b÷c) 与 a×b÷c)的复杂性,我们花费了大量时间进行逻辑校验和边界测试。
结对感受与闪光点分享:
黄奕鸣的结对感受:
在这次结对编程中,我主要负责核心算法模块的设计,包括 Number 类(分数处理)、Expr 类(表达式树构建)、递归生成器 generate_expr 以及去重逻辑 normalize 的实现。在协作中,我深切感受到了“1+1>2”的力量。当我陷入括号生成与结合性处理的逻辑死胡同时,是钱世豪同学在解析器的测试反馈中给了我灵感,让我通过优化的 _format_child 函数彻底解决了歧义问题。
- 对同伴的闪光点与建议:钱世豪同学的代码规范非常好,注释清晰,并且极其细心。他负责的
argparse命令行解析和判分模块grade逻辑严密,极大地提升了程序的健壮性。建议在未来的开发中,我们能在接口定义阶段更早地对齐参数规范,这样可以减少后期代码整合时的返工。
钱世豪的结对感受:
我主要负责程序的命令行参数接收、文件I/O操作(生成 Exercises.txt、Answers.txt 以及判分输出 Grade.txt)、解析器(tokenize / parse_expr)以及后期的全面测试工作。通过这次结对,我不仅巩固了文件操作和异常处理的知识,更从黄奕鸣同学身上学到了如何用树形结构优雅地处理数学表达式和递归计算。
- 对同伴的闪光点与建议:黄奕鸣同学在算法设计上逻辑极其严密,对性能优化的追求令人敬佩。他设计的
normalize函数非常巧妙,成功将一万道题目的生成时间控制在秒级。建议后续可以增加更多的单元测试覆盖,尤其是针对带分数输入输出(如2'3/8)的边界测试。
总结:
这次结对作业不仅锻炼了我们的编码能力,更锻炼了我们的沟通与协作能力。从最初的架构设计,到中期的代码Review,再到后期的测试查漏补缺,我们互相学习,共同进步,最终完成了这份令人满意的答卷。
浙公网安备 33010602011771号