软件工程结对项目 3:python实现自动生成小学四则运算题目的程序
这个作业属于哪个课程 | 广工计院计科34班软工 |
---|---|
这个作业要求在哪里 | 作业要求 |
团队成员1 | 庄崇立3122004633 |
团队成员2 | 罗振烘3122004748 |
这个作业的目标 | 结对合作完成小学四则运算题目的程序,熟悉项目开发流程,提高团队合作能力 |
一、GitHub地址
二、需求
1.题目:实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。
2.说明:
自然数:0, 1, 2, …。
真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
运算符:+, −, ×, ÷。
括号:(, )。
等号:=。
分隔符:空格(用于四则运算符和等号前后)。
算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
四则运算题目:e = ,其中e为算术表达式。
3.需求:
使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10
将生成10个题目。
使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
每道题目中出现的运算符个数不超过3个。
程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,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,它们之间不能通过有限次交换变成同一个题目。
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
四则运算题目1
四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
答案1
答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
程序应能支持一万道题目的生成。
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
三、PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 65 | 85 |
Estimate | 估计这个任务需要多少时间 | 745 | 755 |
Development | 开发 | 305 | 255 |
Analysis | 需求分析 (包括学习新技术) | 35 | 26 |
Design Spec | 生成设计文档 | 30 | 25 |
Design Review | 设计复审 | 25 | 35 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 35 |
Design | 具体设计 | 56 | 46 |
Coding | 具体编码 | 50 | 70 |
Code Review | 代码复审 | 20 | 20 |
Test | 测试(自我测试,修改代码,提交修改) | 50 | 40 |
Reporting | 报告 | 40 | 20 |
Test Repor | 测试报告 | 40 | 50 |
Size Measurement | 计算工作量 | 10 | 10 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 1456 | 1562 |
四、程序设计实现过程
1,框架设计
- 这个项目采用了模块化的设计,将功能逻辑、用户界面和程序入口分离,形成了清晰的三层结构
1.1,核心功能模块(function.py):
- CustomMathError 类:自定义异常类,用于处理特定的数学相关错误。
- Number 类:表示一个数字,包括整数部分和分数部分。它能够生成随机数,并且可以简化分数。
- Fraction 类:处理两个 Number 对象之间的分数运算,支持加减乘除。
- Expression 类:生成和计算数学表达式,是整个程序的核心。
1.2,用户界面模块(interface.py):
- MathQuizApp 类:创建图形用户界面,管理用户交互和显示结果。
1.3,主程序入口(main.py):
- 创建 Tkinter 根窗口并启动应用。
1.4,三个模块之间的关系
- main.py 作为程序入口,它导入并实例化 interface.py 中的 MathQuizApp 类。
- MathQuizApp 类在生成题目时,会创建并使用 function.py 中的 Expression 类。
- Expression 类内部使用 Number 和 Fraction 类来生成和计算表达式。
2,代码部分
- 我们更深入地分析项目的关键代码
2.1,function.py 中的 Number 类
- python核心代码如下
class Number:
def __init__(self, max: int = None, nums: tuple = None):
"""
初始化一个Number对象
:param max: 随机生成数字时的最大值
:param nums: 用于直接指定数字的组成部分(整数,分子,分母)
"""
try:
if nums:
# 如果提供了nums元组,直接用它来初始化数字
if isinstance(nums, tuple) and len(nums) == 3:
if nums[2] == 0:
raise ValueError("分母不能为零。")
else:
self.integer = int(nums[0]) # 整数部分
self.numerator = int(nums[1]) # 分子
self.denominator = int(nums[2]) # 分母
else:
raise ValueError("nums必须是一个三元组(整数,分子,分母)。")
else:
# 如果没有提供nums,则随机生成一个数字
if (not isinstance(max, int)) or max < 1:
raise ValueError("max必须大于等于1。")
self.integer = random.randint(0, max-1) # 随机生成整数部分
self.denominator = random.randint(1, max-1) # 随机生成分母
is_fra = random.randint(1, 2) # 随机决定是否生成分数部分
if is_fra == 1:
self.numerator = random.randint(1, self.denominator) # 生成分子
else:
self.numerator = 0 # 不生成分数部分
# 计算生成的数值
generated_value = self.integer + self.numerator / self.denominator
# 如果生成的值大于等于 max,调整整数部分使其小于 max
if generated_value >= max:
adjustment = int(generated_value // max)
self.integer -= adjustment
except ValueError as e:
raise CustomMathError(str(e))
def reduce_fraction(self):
"""简化分数"""
# 计算最大公约数
gcd = math.gcd(self.numerator, self.denominator)
numerator = self.numerator // gcd
denominator = self.denominator // gcd
# 处理假分数,将其转化为带分数
integer = numerator // denominator + self.integer
numerator = numerator % denominator
self.integer = int(integer)
self.numerator = int(numerator)
self.denominator = int(denominator)
def __str__(self):
"""将Number对象转换为字符串表示"""
if self.integer == 0:
if self.numerator:
return f"{self.numerator}/{self.denominator}"
else:
return '0'
else:
if self.numerator:
return f"{self.integer}'{self.numerator}/{self.denominator}"
else:
return str(self.integer)
def __float__(self):
"""将Number对象转换为浮点数"""
return self.integer + self.numerator / self.denominator
-
Number 类的设计很巧妙:
-
- 它可以通过 max 参数随机生成一个不超过 max 的数,也可以通过 nums 元组直接指定数值。
-
- 生成的数字包含整数部分和分数部分,这样可以生成更多样的题目。
-
- reduce_fraction 方法用于简化分数,保证生成的题目更加规范。
-
整体流程图如下
2.2,function.py 中的 Expression 类
- python核心代码如下
class Expression:
def generate_expression_list(self):
"""生成表达式列表"""
expression_list = []
sub_num = random.randint(1, 3) # 随机决定子表达式的数量
operators = [random.choice([' + ', ' - ', ' * ', ' % ']) for _ in range(sub_num)]
subexpression_1 = self.generate_subexpression(1, operators[0])
str_sub_1 = str(subexpression_1[0]) + subexpression_1[1] + str(subexpression_1[2])
if sub_num == 1:
expression_list.append(subexpression_1)
str_sub_2 = ''
str_sub_3 = ''
string = str_sub_1 + '|' + str_sub_2 + '|' + str_sub_3
elif sub_num == 2:
subexpression_2 = self.generate_subexpression(2, operators[1])
str_sub_2 = str(subexpression_2[0]) + str(subexpression_2[1])
str_sub_3 = ''
expression_list.append(subexpression_1)
expression_list.append(subexpression_2)
string = str_sub_1 + '|' + str_sub_2 + '|' + str_sub_3
else:
etype = random.randint(1, 2)
subexpression_2 = self.generate_subexpression(etype, operators[1])
if etype == 1:
subexpression_3 = [operators[2]]
str_sub_2 = str(subexpression_2[0]) + str(subexpression_2[1]) + str(subexpression_2[2])
str_sub_3 = subexpression_3[0]
if str_sub_1 >= str_sub_2 and operators[2] in [' + ', ' * ']:
expression_list.append(subexpression_1)
expression_list.append(subexpression_2)
expression_list.append(subexpression_3)
string = str_sub_1 + '|' + str_sub_2 + '|' + str_sub_3
else:
expression_list.append(subexpression_2)
expression_list.append(subexpression_1)
expression_list.append(subexpression_3)
string = str_sub_1 + '|' + str_sub_2 + '|' + str_sub_3
else:
subexpression_3 = self.generate_subexpression(2, operators[2])
str_sub_2 = str(subexpression_2[0]) + str(subexpression_2[1])
str_sub_3 = str(subexpression_3[0]) + str(subexpression_3[1])
expression_list.append(subexpression_1)
expression_list.append(subexpression_2)
expression_list.append(subexpression_3)
string = str_sub_1 + '|' + str_sub_2 + '|' + str_sub_3
return expression_list, string
- 这个方法是生成表达式的核心:
-
- 它随机决定生成1到3个子表达式。
-
- 对于每个子表达式,它随机选择运算符,并调用 generate_subexpression 方法生成数字。
-
- 它还处理了不同数量子表达式的组合方式,确保生成的表达式合理且多样。
- 整体简化流程图如下
2.3,interface.py 中的 MathQuizApp 类
- python核心代码如下
class MathQuizApp:
def generate_questions(self):
try:
num_questions = self.validate_input(self.num_questions_entry.get())
max_value = self.validate_input(self.max_value_entry.get())
exp = Expression(max_value, num_questions)
self.questions, self.answers = exp.run()
self.questions_text.delete(1.0, tk.END)
self.answers_text.delete(1.0, tk.END)
for i, question in enumerate(self.questions, start=1):
self.questions_text.insert(tk.END, f"{i}. {question}\n")
with open("Exercises.txt", "w") as question_file:
question_file.writelines(f"{q}\n" for q in self.questions)
with open("Answer.txt", "w") as answer_file:
answer_file.writelines(f"{a}\n" for a in self.answers)
except ValueError as e:
self.show_error(str(e))
def check_answers(self):
user_answers = self.answers_text.get(1.0, tk.END).splitlines()
correct = []
incorrect = []
for i, (user_answer, correct_answer) in enumerate(zip(user_answers, self.answers), start=1):
if user_answer.strip() == correct_answer.strip():
correct.append(i)
else:
incorrect.append(i)
self.results_text.delete(1.0, tk.END)
self.results_text.insert(tk.END, f"正确题号: {', '.join(map(str, correct))}\n")
self.results_text.insert(tk.END, f"错误题号: {', '.join(map(str, incorrect))}\n")
with open("Grade.txt", "w") as grade_file:
grade_file.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
grade_file.write(f"Wrong: {len(incorrect)} ({', '.join(map(str, incorrect))})\n")
- 这些方法展示了 GUI 的核心功能:
-
- generate_questions 方法获取用户输入,使用 Expression 类生成题目,然后显示题目并保存到文件。
-
- check_answers 方法比较用户答案和正确答案,显示结果并保存到文件。
- 完整流程图如下
五、单元测试
1,测试部分概述
为保证该项目代码质量和正确运行,设计者编写了单元测试代码。本次单元测试使用了unittest框架实现对项目的单元测试。测试文件test_function.py
内是对功能代码的测试,包含5条测试用例,测试文件test_interface.py
内是针对用户图形化界面代码的测试,包含7条测试用例。总共12条测试用例
2,测试点
根据项目的代码整理出以下测试点
3,部分测试代码
test_function.py
部分代码
import unittest
from function import Number, Fraction, Expression, CustomMathError
class TestMathFunctions(unittest.TestCase):
def test_number_initialization(self):
# Test 1: Initialize Number with max value
num1 = Number(10)
self.assertIsInstance(num1, Number)
self.assertLessEqual(float(num1), 11) # 检查是否小于等于 11
# Test 2: Initialize Number with specific values
num2 = Number(nums=(3, 1, 2))
self.assertEqual(str(num2), "3'1/2")
test_interface.py
部分代码
import unittest
from unittest.mock import patch, MagicMock
import tkinter as tk
from interface import MathQuizApp
class TestMathQuizApp(unittest.TestCase):
def setUp(self):
self.root = tk.Tk()
self.app = MathQuizApp(self.root)
def tearDown(self):
self.root.destroy()
@patch('interface.Expression')
def test_generate_questions(self, mock_expression):
# 测试用例 1: 正常生成题目
mock_expression.return_value.run.return_value = (["1 + 1", "2 * 2"], ["2", "4"])
self.app.num_questions_entry.insert(0, "2")
self.app.max_value_entry.insert(0, "10")
self.app.generate_questions()
self.assertEqual(self.app.questions_text.get("1.0", tk.END).strip(), "1. 1 + 1\n2. 2 * 2")
def test_validate_input(self):
# 测试用例 2: 有效输入
self.assertEqual(self.app.validate_input("5"), 5)
4, 运行测试用例
12条测试用例全部通过
六、性能分析
在用户图形化界面输入生成10000条题目,对此过程进行性能分析
根据执行结果可以得知,执行时间最长的函数为generate_questions,执行时间为599ms
七、项目总结
1,项目分工:
庄崇立:负责大部分主体代码和测试代码的编写
罗振烘:负责小部分代码编写和博客的编写
2,项目收获
2.1,技术方面的收获
- 对 Python 语言的掌握更加深入。在项目过程中,我们运用了 Python 的各种特性,和各种库函数,提升了编程能力。
- 学会了如何设计和实现一个具有特定功能的程序。从需求分析到算法设计,再到代码实现和测试,我们经历了完整的软件开发流程,积累了宝贵的实践经验。
- 了解了如何进行程序优化。为了提高程序的性能和效率,我们不断尝试不同的方法,如优化算法、减少不必要的计算等,增强了问题解决的能力。
2.2,团队协作方面的收获
- 高效沟通至关重要。在项目中,我们通过频繁的交流,及时分享彼此的想法和进展,避免了误解和重复工作,确保项目顺利进行。
- 优势互补能提升整体效率。我们两人在不同方面各有优势,通过合理分工,充分发挥各自的长处,大大提高了项目的开发速度和质量。
- 学会了互相支持和鼓励。在遇到困难时,我们互相提供帮助和建议,增强了团队的凝聚力。