Github项目地址:Github Pages

结对项目成员:张鹏 3118004985 郑靓 3118004988


一、项目需求分析

二、功能实现


三、代码实现or功能说明

★ GUI功能扩展说明

  • 采用了多线程的界面,任何操作不会阻塞其他操作,例如:可以在生成答案的同时批改作业
  • 得益于上面的设计,可以同时生成多个表达式文件,存储形式如下所示

  • 对于错误的输入,会有提示,如下所示

  • 对于文件选择后,点击批改,对于文件的格式有错误检查

通过后缀表达式的计算过程,确保生成表达式满足题目所有要求,避免重复的表达式生成 (详参下文 '判断重复的思路' )

★ 多线程(防止I/O阻塞)

  • 创建生产者线程, 传参进队列 'queue'

producer = multiprocessing.Process(target=self.expression_generator, args=(queue,))

  • 创建消费者进程, 传参进队列 'queue'

consumer = multiprocessing.Process(target=self.io_operation, args=(queue,))

  • 生产者——循环生成表达式 及其答案

    1. 构建随机表达式 以及生成其答案 ' Arithmetic(self.domain).create_arithmetic() '
    2. 生成其表达式对应答案 ' Calculate(expression).cal_expression() '
    3. 将生成后缀表达式过程中每次的结果 以及操作符集合 保存到 字典 (' self.no_repeat_dict ' ) 中, 从而确保生成等式不相同 (即 3+2+1 与 1+2+3 不相等, 6×8 与 8×6 相等)
    4. 生成完成后, 把表达式 以及 答案添加到队列 queue
  • 消费者——循环生成表达式 及其答案
    1. 通过死循环不断获取队列内容, 若队列传出 'None' 信号, 消费者进程停止
    2. 解析从队列获取的内容, 并将多次获取的表达式以及答案保存到 缓冲区(Buffer) 中, 有限次数后开始写入文件 并 销毁缓冲区内容

★ 判断重复的思路

  1. 由于考虑到题目说1+2+32+1+3相等,1+2+33+2+1是不相等的,我一开始是从字符串的处理考虑,但是复杂度有点高。
  2. 所以换了一个角度考虑,从运算顺序入手,就想到用后缀表达式进行去重,并且这样也不用考虑括号,符合题目所说的(1+2)+31+2+3相等
  3. 具体就是存储每一次运算出来的结果,然后进行一一比较

    例如(这里举的是比较简单的例子): 1+2+3,压入的数字:[3, 6]; 3+2+1,压入的数字:[5,6],所有两个判断为不相等
  4. 但是这样会出现1+32+2判断为重复的情况,所以添加两个数组——[操作数],[运算符],作为比较的依据
  5. 再来考虑效率,用字典的数据结构,以答案为键,其他三个比较标志作为值,只在答案相等的情况下判重

    附:最终选定了添加后缀计算的去重模式,就是为了避免 (1÷1)+31+(3÷1) 这种不为重复表达式的情况,但是效率确实比只判断(操作数、运算符)的模式低了

——创建数据结构

  1. # 用答案作为索引构建的字典,
  2. {
  3. "1'2/2": [
  4. [[压入的数字], [操作数], [运算符]],
  5. [[压入的数字], [操作数], [运算符]],
  6. ...
  7. ]
  8. }
  1. # 通过比较上述字典, 确认新表达式是否已经在上述字典中
  2. def judge_repeat(self, answer, test_sign):
  3. for expression_sign in self.no_repeat_dict[answer]:
  4. # 记录相同的个数
  5. same_num = 0
  6. for i in range(3):
  7. if collections.Counter(expression_sign[i]) == collections.Counter(test_sign[i]):
  8. same_num += 1
  9. # 如果中间结果、操作数、运算符均相等,则为重复
  10. if same_num == 3:
  11. return False
  12. return True

★ 生成表达式思路

  1. # 表达式列表形式
  2. ['10', '÷', '(', '8/9', '÷', '51', ')']
  1. 随机生成操作数列表,运算符列表
  2. 根据以上两个列表构建无括号表达式
  3. 根据运算符个数,随机生成括号个数,最大个数为( 1->0, 2->1, 3->2 )
  4. 再随机括号位置,维护操作数位置列表,插入括号
  1. # 生成表达式
  2. def create_arithmetic(self):
  3. # 生成随机操作数、运算符列表
  4. self.create_operand_list()
  5. self.create_operator_list()
  6. i = 0
  7. # 构建表达式列表
  8. self.expression_split.append(self.operand_list[i])
  9. self.expression_split.append(self.operator_list[i])
  10. i += 1
  11. while i < len(self.operator_list):
  12. self.expression_split.append(self.operand_list[i])
  13. self.expression_split.append(self.operator_list[i])
  14. i += 1
  15. self.expression_split.append(self.operand_list[i])
  16. # 插入括号
  17. if self.operator_num != 1:
  18. bracket_num = random.randint(1, self.operator_num - 1)
  19. self.insert_bracket(bracket_num)
  20. # 删除无用括号
  21. self.del_useless_bracket()
  22. return [self.expression_split, self.operand_list, self.operator_list]

★ 计算思路(后缀表达式)

生成后缀表达式

  1. 设置两个栈,一个用以存储运算符,一个用以存储后缀表达式
  2. 循环遍历表达式列表,如果是操作数,则加入后缀栈
  3. 否则如果是运算符则进入以下判断
    • 如果运算符栈为,或者栈顶为 ( ,则压入运算符栈
    • 否则如果当前运算符大于栈顶运算符的优先级,则压入运算符栈
    • 否则弹栈并压入后缀栈直到优先级大于栈顶或空栈
  4. 否则如果遇到括号则进入以下判断
    • 若为 ( 直接压入运算符栈
    • 否则弹栈并压入后缀栈直到遇到 (
  5. 将运算符栈剩余的元素压入后缀栈

计算后缀表达式

  1. 用一个栈(calculate_stack)作为计算中介
  2. 循环遍历后缀表达式,若为数字压入 calculate_stack
  3. 否则从 calculate_stack 弹出两个数字,分别化为分数类,进行计算,结果压入 calculate_stack
  4. 重复 2-3,若期间运算结果出现负数,或除数为0,则返回false
  5. 直至后缀表达式遍历完成,返回 calculate_stack 的栈顶

代码

  1. class Calculate(object):
  2. def __init__(self, expression):
  3. self.expression = expression
  4. # 分数加法 a1/b1 + a2/b2 = (a1b2 + a2b1)/b1b2
  5. @staticmethod
  6. def fraction_add(fra1, fra2):
  7. molecular = fra1.molecular * fra2.denominator + fra2.molecular * fra1.denominator
  8. denominator = fra1.denominator * fra2.denominator
  9. return Fraction(molecular, denominator)
  10. # 分数减法 a1/b1 - a2/b2 = (a1b2 - a2b1)/b1b2
  11. @staticmethod
  12. def fraction_minus(fra1, fra2):
  13. molecular = fra1.molecular * fra2.denominator - fra2.molecular * fra1.denominator
  14. denominator = fra1.denominator * fra2.denominator
  15. return Fraction(molecular, denominator)
  16. # 分数乘法 a1/b1 * a2/b2 = a1a2/b1b2
  17. @staticmethod
  18. def fraction_multiply(fra1, fra2):
  19. molecular = fra1.molecular * fra2.molecular
  20. denominator = fra1.denominator * fra2.denominator
  21. return Fraction(molecular, denominator)
  22. # 分数除法 a1/b1 ÷ a2/b2 = a1b2/a2b1
  23. @staticmethod
  24. def fraction_divide(fra1, fra2):
  25. molecular = fra1.molecular * fra2.denominator
  26. denominator = fra1.denominator * fra2.molecular
  27. return Fraction(molecular, denominator)
  28. # 基本运算选择器
  29. def operate(self, num1, num2, operater):
  30. if not isinstance(num1, Fraction):
  31. num1 = Fraction(num1)
  32. if not isinstance(num2, Fraction):
  33. num2 = Fraction(num2)
  34. # 计算结果
  35. if operater == '+':
  36. return self.fraction_add(num1, num2)
  37. if operater == '-':
  38. return self.fraction_minus(num1, num2)
  39. if operater == '×':
  40. return self.fraction_multiply(num1, num2)
  41. if operater == '÷':
  42. return self.fraction_divide(num1, num2)
  43. # 转成逆波兰
  44. def generate_postfix_expression(self):
  45. # 运算符栈
  46. operator_stack = []
  47. # 后缀栈
  48. postfix_stack = []
  49. for element in self.expression:
  50. # 如果是操作数则添加
  51. if element not in operators:
  52. postfix_stack.append(element)
  53. # 如果是运算符则按优先级
  54. elif element in operator.values():
  55. # 运算符栈为空,或者栈顶为(,则压栈
  56. if not operator_stack or operator_stack[-1] == '(':
  57. operator_stack.append(element)
  58. # 若当前运算符优先级大于运算符栈顶,则压栈
  59. elif priority[element] >= priority[operator_stack[-1]]:
  60. operator_stack.append(element)
  61. # 否则弹栈并压入后缀队列直到优先级大于栈顶或空栈
  62. else:
  63. while operator_stack and priority[element] < priority[operator_stack[-1]]:
  64. postfix_stack.append(operator_stack.pop())
  65. operator_stack.append(element)
  66. # 如果遇到括号
  67. else:
  68. # 若为左括号直接压入运算符栈
  69. if element == '(':
  70. operator_stack.append(element)
  71. # 否则弹栈并压入后缀队列直到遇到左括号
  72. else:
  73. while operator_stack[-1] != '(':
  74. postfix_stack.append(operator_stack.pop())
  75. operator_stack.pop()
  76. while operator_stack:
  77. postfix_stack.append(operator_stack.pop())
  78. return postfix_stack
  79. # 计算表达式(运算过程出现负数,或者除数为0,返回False,否则返回Fraction类)
  80. def cal_expression(self):
  81. # 生成后缀表达式
  82. expressions_result = self.generate_postfix_expression()
  83. # 存储阶段性结果
  84. stage_results = []
  85. # 使用list作为栈来计算
  86. calculate_stack = []
  87. # 后缀遍历
  88. for element in expressions_result:
  89. # 若是数字则入栈, 操作符则将栈顶两个元素出栈
  90. if element not in operators:
  91. calculate_stack.append(element)
  92. else:
  93. # 操作数
  94. num1 = calculate_stack.pop()
  95. # 操作数
  96. num2 = calculate_stack.pop()
  97. # 除数不能为0
  98. if num1 == "0" and element == '÷':
  99. return [False, []]
  100. # 结果
  101. result = self.operate(num2, num1, element)
  102. if result.denominator == 0 or '-' in result.to_string():
  103. return [False, []]
  104. stage_results.append(result.to_string())
  105. # 结果入栈
  106. calculate_stack.append(result)
  107. # 返回结果
  108. return [calculate_stack[0], stage_results]

四、实际测试

通过命令行控制

python ArithmeticCLMode.py [args|args]

[args]

├─ -h --help # 输出帮助信息

├─ -n # 指定生成表达式数量,默认100

├─ -r # 指定生成表达式各个数字的取值范围,默认100

├─ -a # 需和-e参数共同使用进行批改,指定答案文件

├─ -e # 需和-a参数共同使用进行批改,指定练习文件

└─ -g # 开启GUI

通过gui控制

python ArithmeticGMode.py

执行代码

  1. python ArithmeticCLMode.py -n 100 -r 100

  1. # 将上述执行生成的 Exercise.txt 中的1~10题的答案改为错误 执行
  2. python ArithmeticCLMode.py -e ./docs/Exercise.txt -a ./docs/Answer.txt


五、效能分析

由Pycharm测试输出性能测试

程序耗时在多线程中的 生成表达式及计算, 以及I/O操作

在值域1000的情况下各生成不同数量级四则运算的耗时测试


六、PSP表格

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

七、总结

优点:

  1. 在此次项目合作中,我们通过 "Notion" 这一个软件完成设计我们的 开发流程、工作分配以及我们的代码规范的设计。我们将需求列出,根据难度不同从而安排开发流程,每个人根据自己能力特出点不同而去做不同的需求,再通过交流约定我们每个人的接口。简化开发流程。
  2. 交流和配合都挺顺畅的

不足:

  1. 开发中各个模块的依赖关系在开发任务中没有处理清楚,导致双方都有空窗期

互评

To 郑靓

能力强,效率高,非常积极主动。能根据自己日常使用的工具提高效率,在实际开发中有明确的开发流程思路,开发过程中有部分函数代码注释思路不清。

To 张鹏

配合和交流能力强,效率高,能主动揽接任务,思维挺好的,但比较被动

四则运算生成命令行程序 (Python)的更多相关文章

  1. myapp——自动生成小学四则运算题目的命令行程序(侯国鑫 谢嘉帆)

    1.Github项目地址 https://github.com/baiyexing/myapp.git 2.功能要求 题目:实现一个自动生成小学四则运算题目的命令行程序 功能(已全部实现) 使用 -n ...

  2. 设置PATH 环境变量、pyw格式、命令行运行python程序与多重剪贴板

    pyw格式简介: 与py类似,我认为他们俩卫衣的不同就是前者运行时候不显示终端窗口,后者显示 命令行运行python程序: 在我学习python的过程中我通常使用IDLE来运行程序,这一步骤太过繁琐( ...

  3. 用什么库写 Python 命令行程序?看这一篇就够了

    作者:HelloGitHub-Prodesire HelloGitHub 的<讲解开源项目>系列,项目地址:https://github.com/HelloGitHub-Team/Arti ...

  4. 手写笔记变PDF-几行代码变命令行程序为图形化界面

    前言 最近发现了一个非常不错的Python类库----Gooey, https://github.com/chriskiehl/Gooey 在它的帮助下我们可以非常方便的将一个命令行程序升级成一个图形 ...

  5. 在 Mac OS X 上创建的 .NET 命令行程序访问数据库 (使用Entity Framework 7 )

    var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...

  6. Node.js 命令行程序开发资料

    Node.js 命令行程序开发教程http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html用Node.js创建命令行工具ht ...

  7. Node: 开发命令行程序

    CLI 的全称是 Command-line Interface (命令行界面),即在命令行接受用户的键盘输入并作出响应和执行的程序. 在 Node.js 中,全局安装的包一般都具有命令行界面的功能,例 ...

  8. Node.js 命令行程序开发教程

    nodejs开发命令行程序非常方便,具体操作方式查看下面几篇文章 http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html ...

  9. C# 控制台程序(命令行程序)设置字体颜色,窗口宽高,光标行数

    控制台程序(命令行程序)设置窗口宽度高度,如下代码: Console.WriteLine(Console.WindowHeight); Console.WriteLine(Console.Buffer ...

随机推荐

  1. SpringBoot集成Dubbo+Zookeeper

    目录 Spring版本 dubbo_zookeeper负责定义接口 dubbo_provider 服务提供者 dubbo_consumer服务使用者 Spring版本 不知道为啥,新创建的Spring ...

  2. 史蒂夫-乔布斯(Steve Jobs)斯坦福大学演讲稿(中英对照)

    这是苹果公司和Pixar动画工作室的CEO Steve Jobs于2005年6月12号在斯坦福大学的毕业典礼上面的演讲稿. Thank you. I'm honored to be with you ...

  3. Python File seek() 方法

    概述 seek() 方法用于移动文件读取指针到指定位置.高佣联盟 www.cgewang.com 语法 seek() 方法语法如下: fileObject.seek(offset[, whence]) ...

  4. PHP mysqli_refresh() 函数

    定义和用法 mysqli_refresh() 函数刷新表或缓存,或者重置复制服务器信息.高佣联盟 www.cgewang.com 语法 mysqli_refresh(connection,option ...

  5. 一本通 高手训练 1781 死亡之树 状态压缩dp

    LINK:死亡之树 关于去重 还是有讲究的. 题目求本质不同的 具有k个叶子节点的树的个数 不能上矩阵树. 点数很少容易想到装压dp 考虑如何刻画树的形状 发现一个维度做不了 所以. 设状态 f[i] ...

  6. java -jar .jar中没有主清单属性

    pom里加上 <build> <plugins> <plugin> <groupId>org.springframework.boot</grou ...

  7. InvalidProgramException: Specifying keys via field positions is only valid for tuple data types

    Run Flink实例时,出现如下错误: 原因:Java程序引用了Scala的Tuple2类 遇到的坑,记录下来!

  8. 学习JDBC这一篇就够了

    配套资料,免费下载 链接: https://pan.baidu.com/s/1CKiwCbQV4FGg_4YMQoebkg 提取码: 7cn3 复制这段内容后打开百度网盘手机App,操作更方便哦 第一 ...

  9. 【BalticOI2003】Gem 题解(树形DP)

    题目大意: 给树上每一个结点赋值(值为正整数),要求相邻结点的权值不能相同.问树上最小权值和.$n\leq 10000$. ------------------------- 设$f[i][j]$表示 ...

  10. CustomPlot 在Qt下 鼠标点击曲线 显示当前坐标

    此次记录主要是为了下次使用时能回忆起来才做得笔记,若有需改进的地方,请不吝珠玉. widget.cpp #include "widget.h" #include "ui_ ...