结对项目:自动生成小学四则运算题目

发布时间 2023-09-28 22:49:41作者: 黄翼山

用Python实现一个自动生成小学四则运算题目的命令行程序

 

软件工程 计科21级12班 (广东工业大学 - 计算机学院)
作业要求 结对项目
作业目标 熟悉结对编程

 

成员

姓名 班级 学号
黄翼山 计算机科学与技术2021级2班 3119004783
扎恩哈尔·吾兰 计算机科学与技术2021级2班 3119000743

 

Gitee仓库链接

https://gitee.com/diaocan/diaocan/tree/master/3119004783


 

PSP表格

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

 

项目要求

  1. 实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)
  2. 程序应能支持一万道题目的生成
  3. 使用 -n 参数控制生成题目的个数,使用 -r 参数控制题目中数值的范围
  4. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2
  5. 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
  6. 每道题目中出现的运算符个数不超过3个。
  7. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如:
    • 23 + 45 = 45 + 23 = 是重复的题目,6 × 8 = 8 × 6 = 也是重复的题目。
    • 3+(2+1)1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)
    • 但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
  8. 生成的题目存入执行程序的当前目录下的Exercises.txt文件
  9. 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件
  10. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt

设计实现过程及代码说明

首先设计算法.

  1. 对于上述要求2、6, 我们规定输入参数-r不能小于10, 可以证明3个操作符和10以内的4个操作数能够组合出至少10000条不重复的表达式;
  2. 对于要求4、5、7, 我采取的策略是 "避免" ;
  3. 对于要求10, 先对各个题目生成正确答案, 再与用户给出答案做字符串比较.
  4. 由于在普通四则运算表达式中存在操作符的优先级差别, 乘除运算优先于加减运算, 括号内的运算又要优先于括号外的运算, 很不方便. 所以我们采用后缀表达式来替代原来的普通四则运算表达式, 使得操作符的书写顺序就是实际的计算顺序.

生成表达式的方法是:

  1. 首先生成一系列基本表达式, 如1 2 + 5 4 × 这种由两个常数和一个符号组成的式子.
  2. 如果生成的表达式数量未达到要求, 在上次生成的表达式末尾追加一个操作数和操作符, 形成新的表达式.
  3. 重复步骤2, 直到生成了足够数量的表达式.

这样生成的表达式将形如ab_c_d_.
 
要避免生成的表达式重复, 关键要避免操作符为+\×ab_的重复, 也就是1 2 +\× = 2 1 +\× 这样的重复. 设想一个规模为n×n的矩阵, 其中每个元素的行索引和列索引分别是某个乘法或加法表达式的第一操作数和第二操作数. 如果存在两个重复表达式, 它们一定处在关于该矩阵主对角线对称的位置上. 生成乘法或加法表达式时, 只要令两个操作数所对应的坐标总是处在这个矩阵的上三角或下三角区域即可保证不取得重复表达式.
 
按这样的想法, 要避免两数相减得负数或两数相除得假分数, 只要保证减法表达式两个操作数对应坐标总是处在矩阵的下三角部分、除法表达式两个操作数对应坐标总是处在矩阵的上三角部分 (不含对角线, 因为那就包含了0 ÷ 0这种非法表达式) 就行了.
 
追加操作符时, 按以下3种情况采取不同策略.

  1. 如果是乘号或加号, 第二操作数可以是n以内任意数字;
  2. 如果是除号, 取大于第一操作数的第二操作数;
  3. 如果是减号, 取小于等于第一操作数的第二操作数

 
然后是设计脚本.
这个脚本有两个功能, 一个是生成题目和相应答案; 一个是根据用户给出的题目文件和答案文件, 检查答案的正误. 实现这些功能的是4个子函数:

  1. 用来生成首个基本表达式的first_expression_creator()
  2. 在原有表达式基础上, 增加操作符的opcode_extender()
  3. 负责把结果写入文本文件的txt_writer()
  4. 对用户输入的题目及答案检查正误的answer_checker()

以及将各子函数串联起来的语句:

if __name__ == '__main__':
    print('\n-n 生成题目的个数\n-e 题目文件名 -a 答案文件名\n')
    cmd_str = input()
    cmd_str = cmd_str.split(' ')
    # 若要生成题目
    if cmd_str[0] == '-n':
        exercises_require = int(cmd_str[-1])
        print('\n-r 操作数范围\n')
        cmd_str = input()
        cmd_str = cmd_str.split(' ')
        operand_limit = int(cmd_str[-1])
        if exercises_require > 10000 or operand_limit < 10 or exercises_require < 1:
            print('\n参数不合理\n')
            exit(1)
        # 初始化答案数组. 长度为需要的题目数
        answer_list = [-1 for i in range(exercises_require)]
        # 生成初级基本表达式
        expression_list, answer_list, previous_loc_range = first_expression_creator(answer_list_in=answer_list,
                                                                                    max_n=operand_limit)
        # 若未达到数量要求, 在初级表达式基础上依次增加操作符
        while previous_loc_range[1] < len(answer_list):
            expression_list, answer_list, previous_loc_range = opcode_extender(answer_list_in=answer_list,
                                                                               max_n=operand_limit,
                                                                               expression_list_in=expression_list,
                                                                               previous_loc_range_in=previous_loc_range)
        # 结果写入txt文件
        txt_writer(expression_list_in=expression_list, answer_list_in=answer_list)
    # 若要判定答案对错
    elif cmd_str[0] == '-e':
        correct_list, wrong_list = answer_checker(exercise_file=cmd_str[1], answer_file=cmd_str[3])
        f = open(file='Grade.txt', mode='w', encoding='utf-8')
        f.write('Correct: ' + str(len(correct_list)) + ' (' + ', '.join(correct_list) + ')\n'
                + 'Wrong: ' + str(len(wrong_list)) + ' (' + ', '.join(wrong_list) + ')\n')
        f.close()

上面代码的执行过程是:

  1. 检查用户的输入参数
  2. 若是命令-n, 执行题目生成功能
    1. 先生成基本表达式和答案
    2. 向已有表达式末尾追加操作符和操作数, 并计算答案, 直到表达式数量达到要求
    3. 将结果写入文本文件
  3. 若是命令-e, 执行答案检查功能
    1. 从用户指定的文件读入表达式及答案. 要求表达式写成后缀表达式, 答案文件不加行序号
    2. 分别取出各表达式的操作数和操作符, 计算正确的答案, 以字符串表示
    3. 与用户给出的答案做字符串比较, 同时记录正确或错误的表达式序号
    4. 将结果写入文本文件

 

测试运行

测试生成10000条10以内表达式. 输入命令

-n 10000
-r 10

得到文件Exercises.txtAnswers.txt. 部分内容截图如下.


答案文件中小数被写成分数形式:

 
从上面生成的题目文件和答案文件中取出若干条, 分别放入文件test_exercises.txttest_answer.txt, 把第三个答案从3改成30.

 
测试答案检查功能. 输入命令

-e test_exercises.txt -a test_answer.txt

在文件Grade.txt中可以看到

Correct: 7 (69, 70, 72, 73, 74, 75, 76)
Wrong: 1 (71)

这个结果符合预期


 

效能分析

利用PyCharm Professional软件的性能分析工具, 统计函数执行时间.

  1. 对于执行答案检查功能时的时间消耗, 沿用测试文件test_exercises.txttest_answer.txt. 除去等待输入的耗时, 剩余执行时间约366ms. 因输入的文件内容太少, 不容易看出性能瓶颈.
  2. 对于题目生成功能的时间消耗, 测试生成10000条20以内表达式. 除去等待输入的耗时, 剩余执行时间约469毫秒. 其中opcode_extender()耗时90ms, txt_writer()耗时23ms, opcode_extender()耗时11ms. 用来生成表达式及答案的部分共耗时101ms, 性能可以接受, 暂时没有优化必要.


 

项目小结

从本次作业中, 我有以下几点体会.
一是两个人应该能有较多的能够共同讨论和工作的时间和空间. 如果两个人干活的时间总是凑不到一块, 那就容易两个人互相不清楚对方在干嘛. 如果实在要在这种情况下工作, 应该协商一个交接机制, 两个人分时地工作, 每人工作结束后要清楚地将自己的工作内容交代给对方.
二是态度要诚恳, 沟通要顺畅. 要尽力完成自己的份内工作, 保持耐心, 承担责任, 避免因工作不顺而引发摩擦.
三是珍惜与他人的合作机会. 结对的两人可能水平相当, 通过合作的过程可以互相学习, 互相提高; 也可能是老手带新手, 这就要发挥传帮带精神, 帮助他人的同时, 或许也能发现自身的不足.