软件工程第三次作业

作业信息:

这个作业属于哪个课程 首页 - 计科23级34班 - 广东工业大学 - 班级博客 - 博客园
这个作业要求在哪里 结对项目 - 作业 - 计科23级34班 - 班级博客 - 博客园
这个作业的目标 与他人合作完成一个结对项目, 在过程中学习结对合作精神

3123004540--区泽明

3123004525--程思远

作业GitHub链接: https://github.com/Gerdiber/Calculator

PSP表格:

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

效能改进:

改进耗时:

改进生成表达式生成树的内存占用: 10 min

改进表达式合法性检查的细节以及性能: 25 min

改进使用正则表达式将表达式改为计算算法可识别形式: 10 min

改进思路;
  1. 在验证表达式合法后生成表达式字符串并随之删除树, 及时回收空间
  2. 表达式检查中考虑细节, 提高程序可靠性
  3. 使用正则表达式匹配带分数并向算法可识别形式转换, 提高效率
性能分析图:

在生成模式中:

ScreenShot_2025-10-22_02-03-54

图中可见, 在10000次题目生成中, 消耗最大的函数为: make_problem 函数

而在批改模式中:

ScreenShot_2025-10-21_22-46-36

图中可见, 在10000个题目答案批改中, 消耗最大的函数为: check_answers 函数

设计实现:

程序整体根据输入参数分为两种模式: 生成模式批改模式

程序有以下公有函数: 表达式计算函数

生成模式中, 程序还有以下类和函数:

树节点类, 数字生成函数 , 数字生成格式化字符串函数 , 表达式生成树生成函数, 表达式合法性检查函数 , 表达式字符串生成函数 , 题目答案生成函数

而在批改模式中, 程序有函数: 答案检查函数

两种模式流程图如下:

生成模式:

ScreenShot_2025-10-22_01-22-23

批改模式:

ScreenShot_2025-10-22_01-27-59

代码说明:

项目关键代码:

1. 表达式生成树合法性检查函数
def check_expr(root):
    """检查表达式合法性"""
    if root == None: # 空树/空节点 则直接返回true
        return True
    LC = check_expr(root.left) # 左子树检查结果
    RC = check_expr(root.right) # 右子树检查结果
    if root.value in ops: # 节点为运算符
        temp_str = f"{root.left.result}{root.value}{root.right.result}"
        temp_result = calc(temp_str) #计算当前子表达式
        if temp_result == None: # 有错误则重新生成
            return False
        if root.value == '−': # 减法情况
            if temp_result < 0: # 结果小于0
                root.left, root.right = root.right, root.left # 左右子树互换
                temp_result = temp_result * -1 # 结果取相反数
        elif root.value == '÷': # 除法情况
            if calc(root.right.result) == 0: # 除数为0
                return False
            if isinstance(temp_result, int) or (isinstance(temp_result, fractions.Fraction)
                                                and temp_result.denominator == 1): # 结果为整数
                return False
        root.result = format_num(temp_result)
    else: # 节点为数字
        root.result = root.value # 运算结果即为节点值
    return True and LC and RC # 左右子树与当前树均合法

思路: 先检验左右子树再检验当前树的根节点 . 检验节点时 , 若节点是运算符节点 , 则检查其减法和除法的合法性 : 减法结果不能小于 0 , 非法则交换该减法节点的左右子树 , 使结果为正 , 使表达式合法化 ; 除法则需要 除数不为 0 且 结果应为分数而非自然数 , 除法非法无法矫正 , 直接返回判为非法. 最后往上递归所有子树和节点的合法性 , 得出整个表达式生成树的合法性 .

2. 题目答案生成函数
def make_problem(max_num, count):
    """生成题目"""
    problems = []  # 题目数组
    answers = []  # 答案数组
    seen = set()  # 合法题目集合


    while len(problems) < count:
        # 决定运算符数量
        op_count = random.randint(1, 3) # 运算符数量
        nums = [make_num(max_num) for _ in range(op_count + 1)] # 运算数字生成
        op_list = [random.choice(ops) for _ in range(op_count)] # 运算符生成
        #生成树根节点
        root = exprNode(op_list[0])
        #根据运算符数量生成
        if op_count == 1: # 单运算符
            root.left = exprNode(nums[0])
            root.right = exprNode(nums[1])
        elif op_count == 2: # 2运算符
            LR = random.randint(0, 1) # 决定运算符在左/右子树
            if LR == 0:
                root.left = exprNode(op_list[1], exprNode(nums[0]), exprNode(nums[1]))
                root.right = exprNode(nums[2])
            else:
                root.right = exprNode(op_list[1], exprNode(nums[0]), exprNode(nums[1]))
                root.left = exprNode(nums[2])
        else: # 3运算符
            root.left = exprNode(op_list[1], exprNode(nums[0]), exprNode(nums[1]))
            root.right = exprNode(op_list[2], exprNode(nums[2]), exprNode(nums[3]))
        # 检查表达式合法性
        if check_expr(root) is False:
            continue
        # 生成字符串并比对是否有重复
        expr = make_expr_str(root)
        deleteTree(root)
        simple_expr=expr.replace(' ','')
        if simple_expr in seen:
            continue
        seen.add(simple_expr)
        # 生成答案
        answer = calc(expr)
        problems.append(f"{expr} = ")
        answers.append(format_num(answer))

    return problems, answers

思路: 先通过随机数生成决定: 运算符数量 n (1~3) , 数字数量 n+1 , n 个随机生成的数 . 接下来对运算符数量进行分情况构建生成树:

​ 1 个运算符: 生成树只有 1 个根节点与 2 个叶子节点 . 将运算符填入根节点 , 数字填入叶子节点即可.

​ 2个运算符: 生成树有 1 个根节点 , 左右子树随机存在一个 , 另一侧则为叶子节点 , 子树侧也携带 2 个叶子节点 . 将 3 个数字填入叶子节点 , 2个运算符填入上层节点即可.

​ 3个运算符: 1 个根节点左右有 2 个子树 , 2个子树也各自有 2 个叶子节点 , 将 4 个数字 与 2 个运算符填入即可.

生成树生成完毕后先检查合法性 , 非法则直接略过重新生成 . 合法则生成题目并检查是否已经生成过 , 是否重复 , 重复则重新生成 . 合法且不重复则通过 , 生成对应答案 , 将题目与答案加入数组中 , 直至数组元素数量达到参数要求.

3. 批改答案函数
def check_answers(ex_file, ans_file):
    """批改答案"""
    pattern = r'^\d+\.\s*'
    with open(ex_file, 'r', encoding='utf-8') as f:
        ex_list = [line.strip() for line in f]  # 题目列表

    with open(ans_file, 'r', encoding='utf-8') as f:
        ans_list = [line.strip() for line in f]  # 答案列表

    right = []  # 正确数组
    wrong = []  # 错误数组

    for i, (ex, ans) in enumerate(zip(ex_list, ans_list), 1):
        expr = re.sub(pattern,'', ex.replace('=', ''))
        ans = re.sub(pattern,'', ans)
        correct_ans = format_num(calc(expr))  # 计算正确答案
        if ans == correct_ans:  # 正确
            right.append(i)  # 将题目编号加入正确数组中
        else:  # 错误
            wrong.append(i)  # 将题目编号加入错误数组中

    return right, wrong

思路: 通过传入的路径参数读取要批改的题目列表与答案列表 , 先使用正则表达式去除题目与答案的题号排除干扰 , 再使用计算函数 calc 计算题目的正确答案并与文件答案对比 , 不一致则认为错误 , 一致则认为正确 , 将批改的题号加入批改结果对应的数组中.

测试运行:

单元测试脚本如下:

import unittest
from unittest.mock import patch
import os
import tempfile
import fractions
import calculator
from calculator import make_num, calc, format_num, check_expr, exprNode, make_problem, check_answers


class TestCalculator(unittest.TestCase):

    def setUp(self):
        """测试前的准备工作"""
        self.temp_dir = tempfile.mkdtemp()
    def tearDown(self):
        """测试后的清理工作"""
        import shutil
        shutil.rmtree(self.temp_dir)


    def test_make_num(self):
        """测试数字生成函数"""
        # 测试多次生成,确保不会崩溃
        for _ in range(100):
            num = make_num(10)
            self.assertIsNotNone(num)
            self.assertIsInstance(num, str)


    def test_calc(self):
        """测试表达式计算函数"""
        # 测试基本运算
        self.assertEqual(calc("3+4"), 7)
        self.assertEqual(calc("3-1"), 2)
        self.assertEqual(calc("5×7"), 35)
        self.assertEqual(calc("6÷2"), 3)
        # 测试分数运算
        self.assertEqual(calc("1/2+1/2"), 1)
        self.assertEqual(calc("1/2×2"), 1)
        # 测试带分数
        self.assertEqual(calc("1'1/2"), fractions.Fraction(3, 2))
        # 测试无效表达式
        self.assertIsNone(calc("1/0"))
        self.assertIsNone(calc("1+"))


    def test_format_num(self):
        """测试数字格式化函数"""
        # 测试整数
        self.assertEqual(format_num(5), "5")
        self.assertEqual(format_num(0), "0")
        # 测试真分数
        self.assertEqual(format_num(fractions.Fraction(1, 2)), "1/2")
        # 测试假分数转换为带分数
        self.assertEqual(format_num(fractions.Fraction(5, 2)), "2'1/2")
        # 测试等于1的分数
        self.assertEqual(format_num(fractions.Fraction(2, 2)), "1")


    def test_check_expr(self):
        """测试表达式检查函数"""
        # 测试有效的表达式
        root = exprNode('+', exprNode('1'), exprNode('2'))
        self.assertTrue(check_expr(root))
        self.assertEqual(root.result, "3")
        # 测试无效的表达式(除零)
        root = exprNode('÷', exprNode('1'), exprNode('0'))
        self.assertFalse(check_expr(root))


    def test_make_problem_small(self):
        """测试生成少量题目"""
        problems, answers = make_problem(10, 5)
        self.assertEqual(len(problems), 5)
        self.assertEqual(len(answers), 5)
        # 检查每个问题都有答案
        for i in range(5):
            self.assertTrue(problems[i].endswith("= "))
            self.assertIsNotNone(answers[i])


    def test_make_problem_large(self):
        """测试生成大量题目"""
        problems, answers = make_problem(10, 100)
        self.assertEqual(len(problems), 100)
        self.assertEqual(len(answers), 100)


    def test_check_answers_correct(self):
        """测试批改正确答案"""
        # 创建测试文件
        ex_file = os.path.join(self.temp_dir, "test_ex.txt")
        ans_file = os.path.join(self.temp_dir, "test_ans.txt")
        with open(ex_file, 'w', encoding='utf-8') as f:
            f.write("1. 1 + 1 = \n")
            f.write("2. 2 × 2 = \n")
        with open(ans_file, 'w', encoding='utf-8') as f:
            f.write("1. 2\n")
            f.write("2. 4\n")
        right, wrong = check_answers(ex_file, ans_file)
        self.assertEqual(len(right), 2)
        self.assertEqual(len(wrong), 0)
        self.assertEqual(right, [1, 2])


    def test_check_answers_wrong(self):
        """测试批改错误答案"""
        # 创建测试文件
        ex_file = os.path.join(self.temp_dir, "test_ex.txt")
        ans_file = os.path.join(self.temp_dir, "test_ans.txt")
        with open(ex_file, 'w', encoding='utf-8') as f:
            f.write("1. 1 + 1 = \n")
            f.write("2. 2 × 2 = \n")
        with open(ans_file, 'w', encoding='utf-8') as f:
            f.write("1. 3\n")  # 错误答案
            f.write("2. 5\n")  # 错误答案
        right, wrong = check_answers(ex_file, ans_file)
        self.assertEqual(len(right), 0)
        self.assertEqual(len(wrong), 2)
        self.assertEqual(wrong, [1, 2])


    def test_check_answers_mixed(self):
        """测试批改混合答案"""
        # 创建测试文件
        ex_file = os.path.join(self.temp_dir, "test_ex.txt")
        ans_file = os.path.join(self.temp_dir, "test_ans.txt")
        with open(ex_file, 'w', encoding='utf-8') as f:
            f.write("1. 1 + 1 = \n")
            f.write("2. 2 × 2 = \n")
            f.write("3. 3 - 1 = \n")
        with open(ans_file, 'w', encoding='utf-8') as f:
            f.write("1. 2\n")  # 正确答案
            f.write("2. 5\n")  # 错误答案
            f.write("3. 2\n")  # 正确答案
        right, wrong = check_answers(ex_file, ans_file)
        self.assertEqual(len(right), 2)
        self.assertEqual(len(wrong), 1)
        self.assertEqual(right, [1, 3])
        self.assertEqual(wrong, [2])


    def test_integration(self):
        """测试完整流程"""
        # 生成题目
        problems, answers = make_problem(10, 10)
        # 保存题目和答案
        ex_file = os.path.join(self.temp_dir, "exercises.txt")
        ans_file = os.path.join(self.temp_dir, "answers.txt")
        with open(ex_file, 'w', encoding='utf-8') as f:
            for i, p in enumerate(problems):
                f.write(f"{i + 1}. {p}\n")
        with open(ans_file, 'w', encoding='utf-8') as f:
            for i, a in enumerate(answers):
                f.write(f"{i + 1}. {a}\n")
        # 批改答案
        right, wrong = check_answers(ex_file, ans_file)
        # 所有答案应该都是正确的
        self.assertEqual(len(wrong), 0)
        self.assertEqual(len(right), 10)


class TestCommandLine(unittest.TestCase):

    @patch('sys.argv', ['calculator.py', '-n', '10', '-r', '10'])
    def test_generate_mode(self):
        # 测试生成模式
        # 由于main函数会使用sys.argv,我们通过patch模拟命令行参数
        # 这里我们检查是否成功解析了参数,并且进入了生成模式
        args = calculator.parser.parse_args()
        self.assertEqual(args.n, 10)
        self.assertEqual(args.r, 10)
        self.assertIsNone(args.e)
        self.assertIsNone(args.a)

    @patch('sys.argv', ['calculator.py', '-e', 'exercises.txt', '-a', 'answers.txt'])
    def test_grade_mode(self):
        # 测试批改模式
        args = calculator.parser.parse_args()
        self.assertIsNone(args.n)
        self.assertIsNone(args.r)
        self.assertEqual(args.e, 'exercises.txt')
        self.assertEqual(args.a, 'answers.txt')


if __name__ == '__main__':
    unittest.main(verbosity=2)

通过12个简单测试例子测试了程序的: 数字生成函数稳定性 , 表达式计算函数准确性 , 数字格式化函数准确性 , 表达式检查函数准确性 , 题目生成函数稳定性 , 批改函数准确性 , 整体流程的稳定性 , 程序接收参数的稳定性

测试覆盖率:

ScreenShot_2025-10-22_11-29-59

可以相信 , 程序是可以正确运行的

项目小结:

​ 在这次结对项目中 , 程思远 负责程序的大体设计和简要编写 , 区泽明负责程序的完善与Debug测试 . 在这个过程中对程序实现的理解方面的交流是关键 , 将二人想法融会贯通 , 最终实现完整程序 . 这个过程中外卖体会到结对合作的工作效率比一人工作更高 , 也更容易发现各自想法的缺陷 ,更早发现问题 , 更高效地解决问题 .

闪光点:

区泽明: 对程序的各环节测试和完善都很细节 , 能快速发现程序的问题所在并加以解决 . 对程序实现的想法也很有启发性 .

程思远; 能快速地理解题目需求并设计出程序大体流程 , 为后面的程序开发提供较为明确的方向 .

posted @ 2025-10-22 12:33  OZM  阅读(6)  评论(0)    收藏  举报