结对项目

1.提交

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479
github仓库链接 https://github.com/1610285721-svg/3123004531_jiedui
成员1 梁法恩 3123004531
成员2 纪泓鑫 3123004529

2.PSP表格

Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
估计这个任务需要多少时间 30 40
需求分析 (包括学习新技术) 60 50
生成设计文档 45 50
设计复审 (和同事审核设计文档) 30 30
代码规范 (为目前的开发制定合适的规范) 20 15
具体设计 60 70
具体编码 300 250
代码复审 45 45
测试(自我测试,修改代码,提交修改) 150 180
测试报告 45 45
计算工作量 15 15
事后总结, 并提出过程改进计划 30 20
合计 830 820

3.效能分析
优化花了2小时
起初:
生成速度慢:随机生成表达式后,大量因 “不合法”(如减法为负、除法为假分数)被丢弃。
重复率高:没有去重逻辑导致生成的题目中存在大量等价表达式,实际有效题目数量不足。
计算开销大:表达式计算时频繁进行字符串解析和格式转换,尤其带括号的复杂表达式,重复计算相同子表达式,浪费资源。
改进:
运算符选择优化:若前一个运算符是 “-” 或 “÷”,下一个运算符强制选择 “+” 或 “×”(避免连续减 / 除导致的无效计算)。
放弃字符串直接比对:设计 “标准化 Key”(结果 + 排序操作数 + 运算符),对加法 / 乘法自动排序操作数。
缓存分数解析结果:解析后缓存结果(如用字典存储 “3'1/2”→Fraction(7,2)),重复解析同一分数时直接复用。
357747cf779bf4bd50726800ba5c99fd
生成算式
bac4e3a255a4f275d5d4c6fae7fde065
批改算式

4.设计实现过程
MathExpression 类
fb53e7df4e826d10ee4feaad633820d5
流程图
imageimageimage

5.关键算法的代码说明
430ebca26c82100a8a4d31a8cfa4b47b

def generate_expression(self):
        """生成逻辑(仅在计算后校验,避免复杂前置校验)"""
        while True:
        # 1. 随机生成操作数(对应流程图“生成操作数”步骤)
        parts = [self.generate_number() for _ in range(self.operator_count + 1)]
        
        # 2. 随机生成运算符(含冲突避免规则,对应“生成运算符”步骤)
        ops = []
        for _ in range(self.operator_count):
            if ops and ops[-1] in ['-', '÷']:
                ops.append(random.choice(['+', '×']))  # 避免连续减/除
            else:
                ops.append(random.choice(self.operators))

        # 3. 构建表达式(含括号,对应“构建表达式”步骤)
        final_expr = self.add_valid_parentheses(parts, ops)

        # 4. 计算并校验结果(对应“调用evaluate_with_fraction”和“合法性校验”步骤)
        result, is_valid = self.evaluate_with_fraction(final_expr)

        # 5. 保存合法表达式或重试(对应流程图分支)
        if is_valid:
            self.expression = final_expr + " ="
            self.answer = self.format_fraction(result)
            break  # 合法则退出循环,否则继续重试

f7efff7df85aaf9b4884f2edfac9e979

  def get_unique_key(self):
  """简单去重逻辑(基于结果+操作数)"""
    # 1. 预处理表达式(去除括号和等号,对应“输入表达式”步骤)
    expr_no_paren = self.expression.replace("(", "").replace(")", "").replace(" =", "")

    # 2. 分词(提取操作数和运算符,对应“分词”步骤)
    tokens = re.findall(r'\d+\'\d+/\d+|\d+/\d+|\d+|[+\-×÷]', expr_no_paren)
    if len(tokens) % 2 == 0:
        return expr_no_paren  # 非法格式直接返回原始串
    nums = [tokens[i] for i in range(0, len(tokens), 2)]  # 提取操作数
    ops = [tokens[i] for i in range(1, len(tokens), 2)]   # 提取运算符

    # 3. 计算结果并格式化(对应“计算表达式结果”步骤)
    result, _ = self.evaluate_with_fraction(expr_no_paren)
    result_str = self.format_fraction(result) if result else "invalid"

    # 4. 操作数排序(针对加法/乘法,对应“运算符是否为+或×”判断)
    if len(ops) == 1 and ops[0] in ['+', '×']:
        # 按数值大小排序操作数(解决交换律问题)
        nums_sorted = sorted(nums, key=lambda x: self.parse_to_fraction(x))
        return f"{result_str}_{' '.join(nums_sorted)}_{ops[0]}"
    else:
        # 非加法/乘法保持原始顺序
        return f"{result_str}_{expr_no_paren}"

6.测试运行

import os
import re
import math
import pytest
from io import StringIO
import contextlib
from fractions import Fraction
from main import MathExpression, generate_exercises, grade_exercises, save_to_file


def test_basic_addition_duplication():
    """测试1:加法交换律去重"""
    exercises, _ = generate_exercises(2, 5)
    add_pairs = []
    helper = MathExpression(5)
    for expr in exercises:
        ops = re.findall(r'[+\-×÷]', expr)
        if '+' in ops and len(ops) == 1:
            nums = re.findall(r'\d+\'\d+/\d+|\d+/\d+|\d+', expr.replace('(', '').replace(')', ''))
            if len(nums) == 2:
                num1 = helper.parse_to_fraction(nums[0])
                num2 = helper.parse_to_fraction(nums[1])
                pair = (min(num1, num2), max(num1, num2))
                add_pairs.append(pair)
    assert len(set(add_pairs)) == len(add_pairs), "加法交换律去重失败"


def test_proper_fraction_division():
    """测试2:除法结果为真分数(强化手动校验)"""
    expr_obj = MathExpression(10)
    while '÷' not in expr_obj.expression:
        expr_obj = MathExpression(10)

    # 仅处理1个除法运算符的题目(避免多运算符干扰)
    expr_str = expr_obj.expression.replace('=', '').replace('(', '').replace(')', '').strip()
    tokens = re.findall(r'\d+\'\d+/\d+|\d+/\d+|\d+|[+\-×÷]', expr_str)
    if len(tokens) != 3 or tokens[1] != '÷':
        pytest.skip(f"跳过多运算符题目:{expr_obj.expression}")

    # 手动计算并验证
    a = expr_obj.parse_to_fraction(tokens[0])
    b = expr_obj.parse_to_fraction(tokens[2])
    manual_result = a / b

    # 验证真分数
    assert manual_result.numerator < manual_result.denominator, \
        f"除法结果非真分数:{tokens[0]} ÷ {tokens[2]} = {manual_result}(分子{manual_result.numerator} ≥ 分母{manual_result.denominator})"
    # 验证程序计算正确
    program_result, is_valid = expr_obj.evaluate_with_fraction(expr_str)
    assert is_valid, f"程序判定除法题目非法:{expr_str}"
    assert program_result == manual_result, \
        f"程序计算错误(程序:{program_result},手动:{manual_result})"


def test_mixed_fraction_subtraction():
    """测试3:减法结果非负"""
    expr_obj = MathExpression(10)
    while '-' not in expr_obj.expression:
        expr_obj = MathExpression(10)

    expr_str = expr_obj.expression.replace('=', '').strip()
    result, is_valid = expr_obj.evaluate_with_fraction(expr_str)
    assert is_valid, f"减法题目非法:{expr_str}"
    assert result >= 0, f"减法结果为负:{expr_str} = {result}"


def test_parentheses_validity():
    """测试4:括号不改变运算结果"""
    expr_obj = MathExpression(10)
    while '(' not in expr_obj.expression:
        expr_obj = MathExpression(10)

    expr_with = expr_obj.expression.replace('=', '').strip()
    expr_without = expr_with.replace('(', '').replace(')', '')
    res_with, _ = expr_obj.evaluate_with_fraction(expr_with)
    res_without, _ = expr_obj.evaluate_with_fraction(expr_without)
    assert res_with == res_without, \
        f"括号改变结果:带括号={res_with},无括号={res_without}"


def test_large_scale_duplication():
    """测试5:大数量生成无重复"""
    exercises, _ = generate_exercises(100, 20)
    unique_keys = set()
    helper = MathExpression(20)
    for expr in exercises:
        helper.expression = expr
        unique_keys.add(helper.get_unique_key())
    assert len(unique_keys) == len(exercises), \
        f"大数量生成存在重复(总{len(exercises)}道,唯一{len(unique_keys)}道)"


def test_answer_format_tolerance():
    """测试6:答案格式容错(UTF-8编码)"""
    fixed_exercise = ["1/2 × 5/3 ="]
    save_to_file('Exercises.txt', fixed_exercise)
    save_to_file('Answers.txt', ["0'5/6"])  # 带分数格式

    success = grade_exercises('Exercises.txt', 'Answers.txt')
    assert success, "批改失败"

    with open('Grade.txt', 'r', encoding='utf-8') as f:
        content = f.read()
        assert "Correct: 1" in content, f"格式容错失败(内容:{content})"


def test_negative_subtraction_filter():
    """测试7:无负数减法题目"""
    exercises, _ = generate_exercises(50, 5)
    helper = MathExpression(5)
    for expr in exercises:
        if '-' in expr:
            res, _ = helper.evaluate_with_fraction(expr.replace('=', '').strip())
            assert res >= 0, f"负数减法:{expr} = {res}"


def test_division_by_zero_prevention():
    """测试8:无除零题目"""
    exercises, _ = generate_exercises(50, 10)
    helper = MathExpression(10)
    for expr in exercises:
        if '÷' in expr:
            nums = re.findall(r'\d+\'\d+/\d+|\d+/\d+|\d+', expr)
            divisor = helper.parse_to_fraction(nums[-1])
            assert divisor != 0, f"除零题目:{expr}"


def test_multi_operator_duplication():
    """测试9:多运算符题目去重"""
    exercises, _ = generate_exercises(2, 10)
    helper = MathExpression(10)
    keys = [helper.get_unique_key() for helper.expression in exercises]
    assert len(set(keys)) == len(keys), f"多运算符题目重复(keys:{keys})"


def test_range_limit_warning():
    """测试10:范围不足时程序正常生成"""
    request_num = 1000
    range_limit = 3  # 极小范围,确保组合有限
    try:
        # 核心验证:调用generate_exercises不抛出异常
        actual_exercises, actual_answers = generate_exercises(request_num, range_limit)
        # 辅助验证:生成的题目和答案数量一致
        assert len(actual_exercises) == len(actual_answers), "题目与答案数量不匹配"
        # 辅助验证:至少生成1道题目
        assert len(actual_exercises) >= 1, "未生成任何题目"
        print(f"范围不足测试通过:程序正常生成{len(actual_exercises)}道题目")
    except Exception as e:
        # 若抛出异常则测试失败
        pytest.fail(f"范围不足时程序崩溃:{str(e)}")

测试覆盖率
image

7.实际花费时间
见前表

8.项目小结
本次项目不仅完成了小学四则运算程序的开发,更让我们掌握了 “需求分析→设计→编码→测试” 的完整流程,以及结对开发中的沟通与协作技巧。未来将把 “先设计后编码”“分阶段测试” 的经验应用到更多项目中,同时针对本次暴露的问题,持续优化算法,提升程序的实用性。
我们采用了本地线下的结对,桌子够大,屏幕够大,给了我们良好的结对环境。
成员1对成员2评价:逻辑思维缜密,在设计函数时,能精准预判减法非负、除法真分数等校验规则,并用栈式计算优雅处理运算符优先级,核心算法的健壮性很强。
成员2对成员1评价:测试意识强,能从用户角度提出 “答案格式容错”“范围不足时的友好提示” 等细节需求,编写的测试用例覆盖了 80% 的核心分支,有效降低了 bug 率。

posted on 2025-10-22 21:42  淼一深  阅读(7)  评论(0)    收藏  举报

导航