用 Python 自动生成小学四则运算题目

发布时间 2023-09-28 13:57:44作者: noblogs
这个作业属于哪个课程 软件工程
这个作业要求在哪里 结对项目
这个作业的目标 熟悉多人协作

成员?‍?:戴子豪3121004649、朱俊荣3121004677

GitHub项目地址?:https://github.com/HaoDavis/FourOperations

效能分析

改进思路

  1. is_same 改用 hash 对照的方法,不需要遍历表达式列表,而是将表达式列表里的每一个表达式都转换成 hash 值,再用新的表达式与之对比,这样不需要重复遍历表达式列表,也不用重复计算某条表达式的 hash ,效率大幅提高。
  2. 在 check_result 函数中,用 zip 函数将表达式列表和答案列表打包成一个元组, 再用 map 函数对每一个元组进行判断,提高了效率。

性能分析图

image

image

设计实现过程

  1. Tree 类:由于要判断两个等式本质上是否是一样的,要先将中缀表达式转换为二叉树,用的是 gnerate_tree 函数,然后判断两棵树本质上是否相同,用的是 is_same_tree 函数。
  2. ExpressionProcess 类:用于处理表达式,包括将中缀表达式转换为后缀表达式,用的是 mid2suffix 函数。以及计算后缀表达式,用的是 calculate_suffix 函数。
  3. Generate 类:就像类名一样,这个类里包含很多生成所需内容的函数,包括生成运算符、生成运算数 generate_operand 、生成并插入括号 generate_parentheses 以及处理分数的格式等。还有 generate_question 函数它通过调用其他函数,生成题目和答案。 最重要的是 is_same 函数,它用 hash 来判断新生成的表达式是否与之前的表达式相同。
    具体实现如下:
    def is_same(self, express_set, expression):
        # 使用哈希表来存储已生成的表达式的哈希值
        hash_set = set()

        # 计算新生成表达式的哈希值
        suffixExpression = ExpressionProcess(expression)
        target_exp_suffix = suffixExpression.re
        target_exp_hash = hash("".join(target_exp_suffix))

        # 如果哈希值已经存在于哈希集合中,表明表达式重复
        if target_exp_hash in hash_set:
            return True

        # 否则,将哈希值添加到哈希集合中,并继续检查集合中的其他表达式
        hash_set.add(target_exp_hash)

        for item in express_set:
            suffixExpression2 = ExpressionProcess(item)
            source_exp_suffix = suffixExpression2.re
            source_exp_hash = hash("".join(source_exp_suffix))

            if source_exp_hash in hash_set:
                return True

        return False
  1. Outcome 类:用写入答案以及实现答案检查。 expression_result 将获取到的答案写入 Answers.txt 文件中,做了改进保证写入的答案是带分数也不是真分数。细节如下:
    def expression_result(self, expressions):
        with open('Answer.txt', 'w', encoding='utf-8') as file:
            for i, exp in enumerate(expressions):
                suffix_expression = ExpressionProcess(exp)
                exp_value = suffix_expression.calculate_suffix()
                flag = float(exp_value)
                exp_value = str(exp_value)
                if '/' in exp_value and flag > 1:
                    a,b = exp_value.split('/')
                    a = int(a)
                    b = int(b)
                    # 转化成真分数
                    c = a//b
                    d = a%b
                    e = str(c)+'’'+str(d)+'/'+str(b)
                    exp_value = e
                result = f"Answer{i + 1}: {exp_value}\n"
                file.write(result)

使用上下文管理器。这确保了文件在不再需要时会被正确关闭。这有助于避免资源泄漏和文件锁定问题。另外,在 check_result 方法检查答案时,用zip函数将表达式列表和答案列表打包成一个元组,并行迭代对比给定的文本答案与正确答案,简化代码并提高可读性。静态方法可以提高代码的可维护性和可重用性,

   @staticmethod
    def check_result(exercisefile, answerfile):
        correct_list, wrong_list = [], []
        exercise_answer = []

        try:
            with open(exercisefile, 'r', encoding='utf-8') as exercise_file, \
                    open(answerfile, 'r', encoding='utf-8') as answer_file:

                for (exp_line, ans_line) in zip(exercise_file, answer_file):
                    exp_match = re.match(r'Question\d+: (.*) =\n', exp_line)
                    ans_match = re.match(r'Answer\d+: (.*)\n', ans_line)

                    if exp_match and ans_match:
                        exp = exp_match.group(1)
                        ans = ans_match.group(1)

                        p = ExpressionProcess(exp)
                        exp_value = p.calculate_suffix()
                        flag = float(exp_value)
                        exp_value = str(p.calculate_suffix())
                        if '/' in exp_value and flag > 1:
                            a, b = exp_value.split('/')
                            a = int(a)
                            b = int(b)
                            # 转化成真分数
                            c = a // b
                            d = a % b
                            e = str(c) + '’' + str(d) + '/' + str(b)
                            exp_value = e
                        exercise_answer.append(exp_value)

                        if ans == exp_value:
                            correct_list.append(len(correct_list) + 1)
                        else:
                            wrong_list.append(len(wrong_list) + 1)

            with open('Grade.txt', 'w+', encoding='utf-8') as grade_file:
                grade_file.write(f'Correct: {len(correct_list)} {correct_list}\n')
                grade_file.write(f'Wrong: {len(wrong_list)} {wrong_list}')

        except IOError:
            print('请检查文件路径是否正确')

代码说明

  1. Gnerator 类中的 fraction_to_str 函数,用于将分数转换为字符串,方便写入文件。
    def fraction_to_str(self, operArray):
        operNum1, operNum2 = operArray

        if operNum2 == 1:
            return operNum1

        if operNum1 > operNum2:
            temp = operNum1 // operNum2
            operNum1 -= temp * operNum2
            return f"{temp}'{operNum1}/{operNum2}"

        return f"{operNum1}/{operNum2}"
  1. ExpressionProcess 类中的 mid2suffix 函数,用于将中缀表达式转换为后缀表达式。算法如下:
    1. 遇到操作数时直接加入集合
    2. 遇到操作符,与栈顶操作符比较优先级如果栈为空,或者栈顶元素为(,直接加入栈。如果优先级比栈顶操作数高,直接加入栈。如果优先级比栈顶操作符低或者相等,则弹出栈顶元素入集合,再次进行对比
    3. 遇到括号时:如果为左括号,直接加入栈。如果为右括号,依次弹出栈顶元素入集合,并且再次对比,直到遇到左括号,弹出栈顶元素不入集合。
    4. 最后将栈顶元素依次弹出入集合。
    def mid2suffix(self):
        """
        中缀表达式转为后缀表达式
        :param: exp: 表达式字符串
        :return: result列表
        """
        if not self.exp:
            return []
        ops_rule = {
            '+': 1,
            '-': 1,
            '×': 2,
            '÷': 2,
        }
        suffix_stack = []  # 后缀表达式结果
        ops_stack = []  # 操作符栈
        infix = self.exp.split(' ')  # 将表达式分割得到单词
        # print(infix)
        for item in infix:
            if item in ['+', '-', '×', '÷']:
                while ops_stack and ops_stack[-1] != '(' and ops_rule[item] <= ops_rule.get(ops_stack[-1], 0):
                    suffix_stack.append(ops_stack.pop())
                ops_stack.append(item)
            elif item == '(':
                ops_stack.append(item)
            elif item == ')':
                while ops_stack and ops_stack[-1] != '(':
                    suffix_stack.append(ops_stack.pop())
                ops_stack.pop()  # 弹出 '('
            else:
                suffix_stack.append(item)

        while len(ops_stack) > 0:
            suffix_stack.append(ops_stack.pop())

        self.re = suffix_stack
        return suffix_stack
  1. ExpressionProcess 类中的 calculate_suffix 函数,用于计算后缀表达式的值。算法如下:
    1. 字符为运算数: 直接入栈(先分析出完整的运算数并将其转换为对应的数据类型)
    2. 字符为操作符: 连续出栈两次,使用出栈的两个数据进行相应计算,并将计算结果入栈 e.g:第一个出栈的运算数为a,第二个出栈的运算数为b,此时的操作符为-,则计算 b-a(a和b顺序不能反),并将结果入栈。
    3. 重复以上步骤直至遍历完成后缀表达式,最后栈中的数据就是中缀表达式的计算结果。
    def calculate_suffix(self):
        """
        后缀表达式求值
        :return 运算结果
        """
        stack_value = []
        for item in self.re:
            # print("item")
            # print(item)
            if item in ['+', '-', '×', '÷']:
                n2 = stack_value.pop()
                n1 = stack_value.pop()
                result = self.rule(n1, n2, item)
                if result < 0 or result == False:
                    return False
                stack_value.append(result)
            else:
                if item.find('/') > 0:
                    attach = 0
                    right = ""
                    if item.find("'") > 0:
                        parts = item.split("'")
                        attach = int(parts[0])
                        right = parts[1]
                    else:
                        right = item
                    parts = right.split('/')
                    result = Fraction(attach * int(parts[1]) + int(parts[0]), int(parts[1]))
                    stack_value.append(result)
                else:
                    stack_value.append(Fraction(int(item), 1))

        return stack_value[0]

测试运行

利用unittest单元测试框架,实现单元测试自动化,以下仅列举部分用例。

  1. 正确情况下的中缀表达式转后缀表达式:
    def test_mid2suffix(self):
        # 测试中缀表达式转后缀表达式
        exp = "1 + 2 + 3"
        exp_process = ExpressionProcess(exp)
        self.assertEqual(exp_process.re, ['1', '2', '+', '3', '+'])
  1. 缺少操作数的中缀表达式转后缀表达式:
    def test_mid2suffix2(self):
        # 测试中缀表达式转后缀表达式
        exp = "1 + 2 +"
        with self.assertRaises(IndexError):
            exp_process = ExpressionProcess(exp)
  1. 正确情况下的后缀表达式求值:
    def test_calculate_suffix(self):
        # 测试后缀表达式求值
        exp = "1 2 + 3 +"
        exp_process = ExpressionProcess(exp)
        self.assertEqual(exp_process.value, 6)
  1. 缺少操作数的后缀表达式求值:
    def test_calculate_suffix2(self):
        # 测试后缀表达式求值
        exp = "1 + 2 "
        with self.assertRaises(ValueError):
            exp_process = ExpressionProcess(exp)
  1. 测试生成题目:
    def test_generate_question(self):
        # 测试生成题目
        gen = Generator(10, 10)
        question = gen.generate_question()
        self.assertEqual(len(question), 10)
  1. 测试生成括号:
    def test_generate_parentheses(self):
        # 测试生成括号
        gen = Generator(10, 10)
        question = gen.generate_parentheses("1 + 2 + 3", 2)
        self.assertEqual("(" in question, True)
  1. 测试中缀表达式转后缀表达式:
    def test_mid2suffix3(self):
        # 测试中缀表达式转后缀表达式
        exp = "1 + 5 × ( 3 + 2 ) - 4 × 5"
        exp_process = ExpressionProcess(exp)
        self.assertEqual(exp_process.re, ['1', '5', '3', '2', '+', '×', '+', '4', '5', '×', '-'])
  1. 测试小数转分数:
    def test_to_fraction(self):
        # 测试小数转分数
        self.assertEqual(Generator.to_fraction(self, 1, 2), [1, 2])
  1. 测试获取公因子:
    def test_get_common_factors(self):
        # 测试获取公因子
        self.assertEqual(Generator.get_common_factors(self, 6), [2, 3, 6])
  1. 测试数字转符号:
    def test_num2symbol(self):
        # 测试数字转符号
        self.assertEqual(Generator.num2symbol(self, 1), "+")

  1. 测试判断重复:
    def test_is_same(self):
        # 测试判断重复
        self.assertEqual(Generator.is_same(self, [], "1 + 2"), False)

项目小结

  • 朱俊荣:is_same 函数一开始用暴力法遍历表达式列表,来寻找是否有与新生成出表达式相同的表达式,但效率很低,因为 is_same 底层调用了便利二叉树的函数。后来将表达式列表里的每一个表达式都转换为一个 hash ,然后对于已经产生 hash 的表达式,在后续的判断中就不需要重新遍历他们,这样就将程序运行时间减半。另外答案检查函数中的 zip 手法也提高了效率。另外,这个项目非常综合,考察到了很多方面的内容,有一些规则的判定并没有现成的算法可以参考,需要自己试错。我的伙伴有很好的 Python 语法功底,也很熟练地使用 git 和一些测试工具,让我们的开发效率大大提高。深刻感受到,很多 bug 若是两个人在一起观察讨论就很容易解决,达到一加一远大于二的效果。
  • 戴子豪:结对项目和个人项目很不一样,与人合作增加了不少的沟通协调成本,前期沟通、项目的规划占用了不少的时间,但好处是有人帮忙,可以各取所长。从中吸取的经验是,尽量少规划早动手,边动手边讨论边解决,但是也不能毫无规划,前期应明确各人任务,避免杂乱无章。

附录

PSP表格

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

参考