软件工程结对项目

这个作业属于哪个课程 首页 - 计科23级34班 - 广东工业大学 - 班级博客 - 博客园
这个作业要求在哪里 结对项目 - 作业 - 计科23级34班 - 班级博客 - 博客园
这个作业的目标 <实现一个自动生成小学四则运算题目的命令行程序>

3223004553 陈文婉
3123004536 刘泽昊

GitHub链接:chenwenwan-git/Calculation at main · chenwenwan-git/chenwenwan-git

PSP表格

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

效能分析

  • 单次统计:python cli.py -r 12 -n 50 --statsPerf.txt示例耗时约3ms。
  • 批量采集:python perf_run.pyperf_data.csv(示例:6组)
    • 10,100 → attempts=100, duplicates=0, time_ms≈7
    • 10,1000 → attempts≈1013, duplicates≈13, time_ms≈43
    • 20,1000 → attempts≈1003, duplicates≈3, time_ms≈44
    • 20,5000 → attempts≈5054, duplicates≈54, time_ms≈236
    • 10,10000 → attempts≈10948, duplicates≈948, time_ms≈466
    • 20,10000 → attempts≈10214, duplicates≈214, time_ms≈467
  • 图表:python plot_perf.py

image-20251021213028157
性能改进思路:

  • 在构建SUB/DIV时优先抽样满足约束的分支以减少重试。
  • 通过规范化键的字符串拼接降低哈希成本。
  • 控制三运算符表达式比例,降低生成失败率。

设计实现过程

项目架构设计

采用了模块化设计,将系统划分为多个功能明确的组件,各模块之间通过清晰的接口进行交互(如下图):
image-20251021213324503

关键数据结构

  • 数值表示:统一使用Python标准库的fractions.Fraction进行精确计算,避免浮点数精度问题。带分数仅在输入/输出显示时转换。
  • 表达式节点:采用二叉树结构表示表达式
    • Num类:存储单个数值(自然数、真分数或带分数)
    • BinOp类:存储二元运算,包含运算符类型和左右子表达式
  • 去重机制:通过key()方法生成规范化的字符串表示,对加法和乘法的操作数进行排序,确保交换律等价的表达式视为同一题目。

关键实现

1. 表达式生成流程

表达式生成采用递归构建二叉树的方法,确保生成的题目满足各种约束条件:

  1. 叶子节点生成

    • 随机决定生成自然数、真分数或带分数
    • 确保数值在指定范围内(-r参数控制)
  2. 非叶子节点构建

    • 随机选择运算符(+、-、×、÷)
    • 递归生成左右子表达式
    • 对于减法和除法,检查约束条件:
      • 减法:被减数 ≥ 减数(确保结果非负)
      • 除法:结果为真分数(0 < 商 < 1)
    • 若约束不满足,重新生成
  3. 去重处理

    • 通过key()方法生成规范化键
    • 使用集合存储已生成表达式的键
    • 若新生成的表达式键已存在,重新生成
  4. 格式化输出

    • 使用to_str()方法生成带括号的表达式字符串
    • 添加编号和格式前缀
    • 写入文件

2. 评分流程

评分模块采用了经典的表达式求值算法,确保准确计算结果并进行比较:

  1. 格式解析

    • 去除行首编号和中文标签
    • 移除末尾的等号
    • 按照空格分割字符串
  2. 符号规范化

    • 将中文运算符(×、÷)映射为Python支持的运算符(*、/)
    • 解析带分数为假分数形式
  3. 表达式求值

    • 使用Shunting-yard算法将中缀表达式转换为逆波兰表达式(RPN)
    • 利用栈计算RPN表达式的值,使用Fraction确保精确计算
  4. 结果比对

    • 将计算结果与用户答案进行比较
    • 记录正确和错误的题目编号
    • 生成评分报告文件

重点运用

  1. 使用Fraction类**:选择Python的fractions.Fraction类进行所有数值计算,避免浮点数精度问题,确保结果的精确性。

  2. 递归构建表达式树:通过二叉树结构表示表达式,便于实现运算符优先级处理、括号添加和去重机制。

  3. 规范化键生成:为了解决去重问题,设计了专门的键生成机制,对满足交换律的运算(加法、乘法)进行操作数排序,确保等价表达式生成相同的键。

  4. 灵活的格式处理:在评分模块中实现了健壮的格式解析功能,支持多种编号格式和分隔符,提高了系统的兼容性。

关键代码说明

1. 命令行接口实现

# cli.py - 核心命令行处理逻辑
def main():
    """主函数:解析命令行参数并执行相应操作"""
    parser = argparse.ArgumentParser(description='小学数学四则运算题目生成器')
    
    # 支持两种用法:直接指定参数或使用子命令
    subparsers = parser.add_subparsers(dest='command', help='可用命令')
    
    # 生成题目子命令
    generate_parser = subparsers.add_parser('generate', help='生成四则运算题目')
    generate_parser.add_argument('-r', type=int, default=10, help='数值范围(默认10)')
    generate_parser.add_argument('-n', type=int, default=10, help='题目数量(默认10)')
    generate_parser.add_argument('--stats', action='store_true', help='输出性能统计信息')
    
    # 评分子命令
    grade_parser = subparsers.add_parser('grade', help='评分四则运算题目')
    grade_parser.add_argument('-e', type=str, required=True, help='题目文件路径')
    grade_parser.add_argument('-a', type=str, required=True, help='答案文件路径')
    
    # 顶层参数(兼容直接使用的情况)
    parser.add_argument('-r', type=int, default=10, help='数值范围(默认10)')
    parser.add_argument('-n', type=int, default=10, help='题目数量(默认10)')
    parser.add_argument('-e', type=str, help='题目文件路径')
    parser.add_argument('-a', type=str, help='答案文件路径')
    parser.add_argument('--stats', action='store_true', help='输出性能统计信息')
    
    args = parser.parse_args()
    
    # 确定执行模式:生成或评分
    if args.command == 'generate' or (args.e is None and args.a is None):
        # 生成模式
        if args.n > 10000:
            print(f"警告:题目数量 {args.n} 较大,可能耗时较长")
        ex_path, ans_path = gen.generate(args.n, args.r, args.stats)
        print(f"已生成题目与答案:")
        print(f"Exercises -> {ex_path}")
        print(f"Answers   -> {ans_path}")
    elif args.command == 'grade' or (args.e is not None and args.a is not None):
        # 评分模式
        efile = args.e
        afile = args.a
        out_grade = grade.grade(exercise_file=efile, answer_file=afile)
        print(f"已生成评分结果:")
        print(f"Grade -> {out_grade}")
    else:
        # 参数不完整,显示帮助信息
        parser.print_help()

2. 表达式生成核心

# gen.py - 递归构建表达式树
def build_expr_with_ops(depth, max_ops) -> Expr:
    """递归构建指定运算符数量的表达式"""
    # 基础情况:如果已达到最大运算符数量或随机决定终止递归,生成叶子节点
    if depth >= max_ops or random.random() < 0.3:
        return random_leaf()
    
    # 递归情况:生成内部节点
    # 决定剩余的运算符如何分配给左右子树
    remaining_ops = max_ops - depth
    left_ops = random.randint(0, remaining_ops)
    right_ops = remaining_ops - left_ops
    
    # 递归生成左右子表达式
    left = build_expr_with_ops(depth + 1, max_ops)
    right = build_expr_with_ops(depth + 1, max_ops)
    
    # 尝试不同的运算符,确保满足约束
    while True:
        op = random.choice(['+', '-', '*', '/'])
        
        # 计算子表达式的值
        left_val = left.evaluate()
        right_val = right.evaluate()
        
        # 检查约束条件
        if op == '-' and left_val >= right_val:
            break  # 减法约束满足
        elif op == '/' and right_val != 0 and 0 < left_val / right_val < 1:
            break  # 除法约束满足
        elif op in ['+', '*']:
            break  # 加减乘无额外约束
        
        # 约束不满足,尝试交换左右子树
        if op == '-' or op == '/':
            left, right = right, left
            left_val, right_val = right_val, left_val
            
            # 再次检查约束
            if (op == '-' and left_val >= right_val) or \
               (op == '/' and right_val != 0 and 0 < left_val / right_val < 1):
                break
    
    return BinOp(op, left, right)

3. 表达式求值与Shunting-yard算法

# grade.py - Shunting-yard算法实现

def _shunting_yard(tokens: List[str]) -> List[str]:
    """使用Shunting-yard算法将中缀表达式转换为后缀表达式(RPN)"""
    # 运算符优先级映射
    precedence = {'+': 1, '-': 1, '*': 2, '/': 2}
    # 输出队列
    output = []
    # 运算符栈
    op_stack = []
    
    for tok in tokens:
        if tok.isdigit() or '/' in tok:
            # 处理数字(整数或分数)
            output.append(tok)
        elif tok == '(':
            # 左括号直接入栈
            op_stack.append(tok)
        elif tok == ')':
            # 右括号:弹出运算符直到遇到左括号
            while op_stack and op_stack[-1] != '(':
                output.append(op_stack.pop())
            # 弹出左括号
            if op_stack and op_stack[-1] == '(':
                op_stack.pop()
            else:
                raise ValueError('括号不匹配')
        elif tok in precedence:
            # 运算符:根据优先级规则处理
            while (op_stack and op_stack[-1] != '(' and 
                   precedence[op_stack[-1]] >= precedence[tok]):
                output.append(op_stack.pop())
            op_stack.append(tok)
        else:
            # 未知token
            raise ValueError(f'未知token: {tok}')
    
    # 处理剩余的运算符
    while op_stack:
        if op_stack[-1] == '(':
            raise ValueError('括号不匹配')
        output.append(op_stack.pop())
    
    return output

def _eval_rpn(rpn: List[str]) -> Fraction:
    """计算后缀表达式(RPN)的值"""
    stack = []
    
    for tok in rpn:
        if tok.isdigit():
            # 整数
            stack.append(Fraction(int(tok)))
        elif '/' in tok:
            # 分数
            num, den = map(int, tok.split('/'))
            stack.append(Fraction(num, den))
        elif tok in '+-*/':
            # 运算符:弹出两个操作数,执行运算
            if len(stack) < 2:
                raise ValueError('表达式格式错误')
            b = stack.pop()
            a = stack.pop()
            
            if tok == '+':
                stack.append(a + b)
            elif tok == '-':
                stack.append(a - b)
            elif tok == '*':
                stack.append(a * b)
            elif tok == '/':
                stack.append(a / b)
    
    if len(stack) != 1:
        raise ValueError('表达式格式错误')
    
    return stack[0]

说明

  • 去重键生成机制:仅在节点层面对+×的左右交换进行规范化
# expr.py - 核心去重逻辑
def key(self) -> str:
    # 递归获取左右子表达式的键
    lk = self.left.key(); rk = self.right.key()
    # 对于加法和乘法(满足交换律的运算),对操作数排序确保等价表达式生成相同键
    if self.op == '+': 
        a, b = sorted([lk, rk])
        return f"A:({a},{b})"  # A表示加法
    if self.op == '*': 
        a, b = sorted([lk, rk])
        return f"M:({a},{b})"  # M表示乘法
    # 对于减法和除法(不满足交换律的运算),保持顺序
    if self.op == '-': 
        return f"S:[{lk},{rk}]"  # S表示减法,使用方括号强调顺序
    if self.op == '/': 
        return f"D:[{lk},{rk}]"  # D表示除法,使用方括号强调顺序

- 对于加法和乘法,无论操作数顺序如何,都会生成相同的键

- 对于减法和除法,由于不满足交换律,所以保留操作数顺序

- 使用不同的前缀和括号样式增强可读性和唯一性

  • 表达式打印与格式控制:智能添加括号,确保运算顺序正确
# expr.py - to_str()方法核心逻辑
def to_str(self, parent_prec=0) -> str:
    # 获取当前运算符优先级
    curr_prec = 1 if self.op in ['+', '-'] else 2
    # 需要添加括号的情况:当前优先级低于父优先级,或同级且右结合
    need_paren = parent_prec > curr_prec or \
                 (parent_prec == curr_prec and \
                  (self.op in ['-', '/'] and parent_is_right))
    
    # 获取左右子表达式的字符串表示(递归调用)
    left_str = self.left.to_str(curr_prec)
    right_str = self.right.to_str(curr_prec)
    
    # 替换运算符显示:×代替*,÷代替/
    display_op = '×' if self.op == '*' else ('÷' if self.op == '/' else self.op)
    
    # 组合表达式字符串
    expr_str = f"{left_str} {display_op} {right_str}"
    
    # 必要时添加括号
    return f"( {expr_str} )" if need_paren else expr_str
  • 分数格式化:支持真分数和带分数的智能显示
# numutil.py - 分数格式化核心函数
def fraction_to_str(f: Fraction) -> str:
    # 确保分数已约分
    f = f.limit_denominator()
    
    # 获取分子和分母
    num, den = f.numerator, f.denominator
    
    # 处理整数情况
    if den == 1:
        return str(num)
    
    # 处理负数情况(将负号放在整数部分)
    if num < 0:
        return f"-{fraction_to_str(Fraction(-num, den))}"
    
    # 处理带分数情况(分子大于分母)
    if num > den:
        whole = num // den  # 整数部分
        frac = Fraction(num % den, den)  # 分数部分
        return f"{whole}’{frac.numerator}/{frac.denominator}"
    
    # 处理真分数情况
    return f"{num}/{den}"
  • gen.py - 在构建表达式时应用约束
while True:
    # 尝试构建表达式
    left = build_expr(...)
    right = build_expr(...)
    
    # 根据运算符类型应用对应约束
    if op == '-':
        if valid_sub(left_val, right_val):
            break  # 约束满足,退出循环
    elif op == '/':
        if valid_div(left_val, right_val):
            break  # 约束满足,退出循环
    else:
        break  # 加法和乘法无特殊约束

测试运行

测试用例

  • 确定性全对用例:testdata/exercises_case1.txt + answers_case1.txtCorrect: 10Wrong: 0
  • 错误示例用例:testdata/exercises_case2.txt + answers_case2.txt(刻意错1、2、8) → Correct: 7Wrong: 3 (1, 2, 8)
  • 随机生成核验:python cli.py -r 12 -n 5 → 生成编号格式的题目与答案;评分全对。
  • 边界与小范围:python cli.py -r 2 -n 10(分数较少但可用)。
  • 兼容性:顶层与子命令两种用法均可;缺少必要参数时输出帮助。
  • 题目格式一致性检查:
    • 运算符与等号前后均有空格;`
    • 显示符号为×/÷
    • 真分数/带分数/整数显示符合要求。

image-20251021214820342

image-20251021214831032

具体运用可见github项目的md执行文档,如下:

使用说明

  • 生成题目(无子命令):
    • python cli.py -r 10 -n 10
    • 可加性能统计:python cli.py -r 20 -n 10000 --stats
  • 生成题目(子命令):
    • python cli.py generate -r 10 -n 10
  • 评分统计(无子命令):
    • python cli.py -e Exercises.txt -a Answers.txt
  • 评分统计(子命令):
    • python cli.py grade -e Exercises.txt -a Answers.txt

运行结果支持10000道题目

image-20251021214558842

image-20251021214620088

image-20251021214631747

生成文件对应在根目录下的Answers,Exercises,Grade相关txt文件

性能测试结构

image-20251021214723812

image-20251021214749612

项目总结

本项目基于命令行实现一个自动生成小学四则运算题目的程序,我们结队完成了包括题目生成、答案计算、去重机制、评分功能,性能估计以及支持大量(一万道)题目的生成,在功能和性能上面不断进行优化,达到了预期目标。
同时,结队完成项目的过程中也有些许不足:
初期计划不足:在项目初期,计划制定不够细致,导致部分模块(如去重逻辑)的设计在编码阶段才逐步完善,增加了开发时间。
测试覆盖率有限:虽然进行了多种测试用例验证,但缺乏更加全面的自动化单元测试,可能遗漏边缘情况。
性能优化空间:尽管性能测试显示生成一万道题目耗时已大幅减少,但对于大规模生成(如 n=10000),仍可通过进一步优化去重算法或减少递归开销来提升效率。

经验教训和收获

在完成项目的过程中,我们认识到以下几点
模块化设计的重要性:通过将程序划分为生成、评分和格式化模块,代码结构清晰,易于维护和扩展。未来项目应继续采用模块化设计。
提前学习新技术:在需求分析阶段提前学习 fractions.Fraction 和 Shunting-yard 算法,显著降低了开发中的技术障碍。
性能测试驱动优化:通过性能测试数据(如 perf_data.csv),我们识别了去重机制的瓶颈,并通过规范化键生成和约束检查优化了生成效率。
更详细的计划:未来应在项目初期制定更详细的开发计划,明确每个模块的功能和接口,减少开发中的临时调整。
加强测试:应增加自动化单元测试,覆盖更多边界情况(如极端数值范围或复杂表达式),以提升代码健壮性。
中间检查点:在开发过程中应设置更多检查点,及时复审代码和设计,避免后期修改成本过高。

结队感受

陈文婉:
这次结队项目我们分工明确,互相配合完成了从需求分析到代码实现的各个阶段,从理清需求到算法设计再到性能优化,都是在一步一步的交流讨论中构建出来的,这个过程很有意义。在测试阶段,队友提出了丰富全面的性能测试和用例,为项目的完整性和优化提供了很好的支持。
刘泽昊:
结队过程很愉快,队友在算法设计上的严谨思路让我受益匪浅,尤其是在去重机制和表达式树构建上的讨论,推动了项目的顺利进展。同时,我也学到了如何在团队中高效沟通和协调任务。通过这次结对项目,我们不仅完成了功能需求,还在协作中锻炼了沟通能力和团队意识,为未来的软件开发积累了宝贵经验。

posted on 2025-10-21 21:59  VOK  阅读(24)  评论(0)    收藏  举报

导航