信息
姓名 | 学号 | GitHub地址 |
---|---|---|
钟学 | 3121005280 | https://github.com/GTzx/project_2 |
陈勇佳 | 3121005292 |
软件工程 | 课程主页 |
---|---|
作业要求 | 作业要求 |
作业目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
需求
题目:实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。
-
使用 -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 [exercisefile.txt] -a [answerfile.txt]
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
模块设计
此项目设计了4个主要模块以用来完成题目要求。
-
generate_number ()
是生成随机数函数,用来生成指定范围的自然数或真分数。 -
generate_expression()
是生成表达式的函数,用来生成满足题目要求的四则表达式。其中该模块还包含三个函数,即生成由一个运算符,两个运算符,三个运算符组成的表达式,分别为generate_one_expression()
generate_two_expression()
generate_three_expression()
-
generate_questions_and_answers ()
是生成题目和答案的函数,把生成的题目和答案保存在列表中以确保后续方便读取。grade_questions ()
是统计正确答案数量的函数。 -
除了这几个主要模块,还有一些用于服务其他模块的函数,例如用来把假分数转换为带分数的
convert_fraction ()
,保存到文件里的函数save_to_file()
,这里不展示其详细代码。
生成随机数模块
通过random库中的randint函数生成指定范围内的随机数(自然数或真分数)
# 生成随机数
def generate_number(range_limit):
if random.random() < 0.5: # 50% 的概率生成真分数
numerator = random.randint(0, range_limit - 1)
denominator = random.randint(numerator + 1, range_limit)
return Fraction(numerator, denominator)
else: # 50% 的概率生成自然数
return random.randint(0, range_limit - 1)
生成表达式模块
随机生成一个运算符个数不超过3个的表达式,通过生成数和运算符,再通过题目要求修改后将数和运算符组成一个表达式字符串。
def generate_expression(range_limit):
if range_limit < 1:
raise ValueError("数值范围必须大于等于1")
while True:
yunsuanfu_num = random.randint(1, 3)
if yunsuanfu_num == 1:
expression, expression_1 = generate_one_expression(range_limit)
elif yunsuanfu_num == 2:
expression, expression_1 = generate_two_expression(range_limit)
else:
expression, expression_1 = generate_three_expression(range_limit)
# 检查是否已生成过这个表达式,如果是则重新生成
if expression not in generated_expressions:
generated_expressions.add(expression)
return expression, expression_1
生成题目和答案模块
将已生成的表达式字符串进行预处理,修改为可运行的运算符,再通过eval函数计算除答案,并将题目和答案保存在相应的列表中。
# 生成题目和答案
def generate_questions_and_answers(num_questions, range_limit):
questions = []
answers = []
for _ in range(num_questions):
expression, expression_1 = generate_expression(range_limit)
decimal_result = eval(expression_1.replace('÷', '/').replace('×', '*'))
# while decimal_result < 0:
# expression, expression_1 = generate_expression(range_limit)
# decimal_result = eval(expression_1.replace('÷', '/').replace('×', '*'))
fraction_result = convert_to_fraction(decimal_result)
if isinstance(fraction_result, int) == False and fraction_result % 1 != 0:
fraction_result = convert_fraction(f"{Fraction(fraction_result).limit_denominator()}")
questions.append(expression)
answers.append(fraction_result)
return questions, answers
统计对错题目模块
输入题目和答案文件,对其预处理后计算题目的答案是否与所给的答案文件里的答案一致,统计对错个数。
# 统计对错题目
def grade_questions(exercise_file, answer_file):
correct_indices = []
wrong_indices = []
try:
with open(exercise_file, "r", encoding='utf-8') as exercises, open(answer_file, "r",
encoding='utf-8') as answers:
for i, (exercise, answer) in enumerate(zip(exercises, answers), start=1):
exercise = exercise.strip()
answer = answer.strip()
# 检查题目行是否以"题目X:"开头(X是题目编号)
if exercise.startswith("题目"):
parts = exercise.split(":", 1)
if len(parts) == 2:
exercise = parts[1].strip()
# 检查答案行是否以"答案X:"开头(X是题目编号)
if answer.startswith("答案"):
parts = answer.split(":", 1)
if len(parts) == 2:
answer = parts[1].strip()
try:
user_answer = eval(answer.replace('‘', '+'))
user_answer = Fraction(user_answer).limit_denominator() # 将小数答案转换为分数表示
correct_answer = eval(add_parentheses(exercise).replace('÷', '/').replace('×', '*'))
correct_answer = Fraction(correct_answer).limit_denominator() # 将小数答案转换为分数表示
if user_answer == correct_answer:
correct_indices.append(i)
else:
wrong_indices.append(i)
except Exception as e:
print(f"在评分第 {i} 题时发生错误:{e}")
return correct_indices, wrong_indices
except FileNotFoundError:
print(f"文件不存在")
return FileNotFoundError
主要模块的流程图
性能分析
在此项目中以生成题目功能作分析,其中参数设置为 -n 1000 -r 10,其中main.py运行时间为101ms,消耗时间最多的函数为generate_questions_and_answers()
,调用一次花费90ms,占比89.1%,自身用时3ms,占比3%。generate_expression()
调用1000次,花费53ms,占比53.5%。由于这两个函数在调用时也会调用其他函数,主要时间也花费在调用其他函数上,自身用时少。所以要改进代码就只能改进最常调用的基本函数,如generate_one_expression()
和generate_number()
等,这些基本函数大多调用内置库运行,可以通过替换更可靠更高效的库,或者在循环条件设置上更加合理,从而减少不必要的循环,减少时间消耗。经过优化,可减少10%~20%的时间消耗。
单元测试与异常处理
测试代码test_main.py
中,共设置了8个单元测试和异常处理测试。
测试generate_number函数生成的数是否在合理范围内
def test_generate_number(self):
# 测试generate_number函数生成的数是否在合理范围内
for _ in range(100):
number = generate_number(10)
self.assertTrue(0 <= number < 10)
测试generate_expression函数生成的表达式是否有重复
def test_generate_expression_equivalence(self):
# 测试generate_expression函数生成的表达式是否有重复
range_limit = 10
num_expressions = 1000
generated_expressions = set()
for _ in range(num_expressions):
expression = generate_expression(range_limit)
# 检查是否有重复的表达式,或者等效的表达式
if expression in generated_expressions:
assert False, f"Duplicate expression found: {expression}"
# 将通过重复检测的表达式加入表达式集合中
generated_expressions.add(expression)
测试convert_to_fraction函数是否正确将小数转换为分数
def test_convert_to_fraction(self):
# 测试convert_to_fraction函数是否正确将小数转换为分数
decimal_values = [0.25, 0.5, 0.75, 1.2, 2.5]
expected_fractions = [Fraction(1, 4), Fraction(1, 2), Fraction(3, 4), Fraction(6, 5), Fraction(5, 2)]
for decimal, expected in zip(decimal_values, expected_fractions):
result = convert_to_fraction(decimal)
self.assertEqual(result, expected)
测试convert_fraction函数是否正确将假分数转换为带分数
def test_convert_fraction(self):
# 测试convert_fraction函数是否正确将假分数转换为带分数
test_num1 = '5/3' # 带分数为 1‘2/3
test_num2 = '34/11' # 带分数为 3’1/11
test_num3 = '59/8' # 带分数为 7‘3/8
test_num4 = '2612/315' # 带分数为 8‘92/315
test_num5 = '383/40' # 带分数为 9‘23/40
converted_test_num1 = '1‘2/3'
converted_test_num2 = '3’1/11'
converted_test_num3 = '7‘3/8'
converted_test_num4 = '8‘92/315'
converted_test_num5 = '9‘23/40'
if convert_fraction(test_num1) == converted_test_num1 and convert_fraction(test_num2) == converted_test_num2 and convert_fraction(test_num3) == converted_test_num3 and convert_fraction(test_num4) == converted_test_num4 and convert_fraction(test_num5) == converted_test_num5:
self.assertTrue(1)
else:
self.assertFalse(0)
测试 add_parentheses 函数是否能正常为所有数加个括号
def test_add_parentheses(self):
# 测试 add_parentheses 函数是否能正常为所有数加个括号
exp1 = "1 - 0 ÷ 4/5 - 3/7" # 加括号后为 (1 )-( 0 )÷( 4/5 )-( 3/7)
exp2 = "7 + 2 - 4 ÷ 3/7" # 加括号后为 (7 )+( 2 )-( 4 )÷( 3/7)
exp3 = "1/4 ÷ 2 - 6/7" # 加括号后为 (1/4 )÷( 2 )-( 6/7)
exp4 = "4/5 × 6 + 1/3" # 加括号后为 (4/5 )×( 6 )+( 1/3)
exp5 = "2 × 5/8" # 加括号后为 (2 )×( 5/8)
pare_exp1 = "(1 )-( 0 )÷( 4/5 )-( 3/7)"
pare_exp2 = "(7 )+( 2 )-( 4 )÷( 3/7)"
pare_exp3 = "(1/4 )÷( 2 )-( 6/7)"
pare_exp4 = "(4/5 )×( 6 )+( 1/3)"
pare_exp5 = "(2 )×( 5/8)"
if add_parentheses(exp1) == pare_exp1 and add_parentheses(exp2) == pare_exp2 and add_parentheses(exp3) == pare_exp3 and add_parentheses(exp4) == pare_exp4 and add_parentheses(exp5) == pare_exp5:
self.assertTrue(1)
else:
self.assertFalse(0)
测试grade_questions函数是否正确统计正确和错误的题目
def test_grade_questions(self):
# 测试grade_questions函数是否正确统计正确和错误的题目
correct_indices, wrong_indices = grade_questions("test_exercises.txt", "test_answers.txt")
self.assertEqual(len(correct_indices), 10)
self.assertEqual(len(wrong_indices), 10)
异常处理-文件不存在的情况
def test_invalid_file_paths(self):
# 文件不存在的情况
self.assertEqual(grade_questions('non_existent_file.txt', 'non_existent_file.txt'), FileNotFoundError)
异常处理-测试生成的表达式是否合法
def test_generate_expression(self):
# 测试generate_expression函数生成的表达式是否合法
for _ in range(100):
expression, expression_1 = generate_expression(10)
if Fraction(eval(expression.replace('÷', '/').replace('×', '*'))) == ZeroDivisionError:
self.assertFalse(0)
else:
self.assertTrue(1)
如果所有测试都通过,则会输出以下信息。
Ran 8 tests in 0.029s
OK
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 80 |
· Estimate | · 估计这个任务需要多少时间 | 300 | 500 |
Development | 开发 | 200 | 300 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 90 |
· Design Spec | · 生成设计文档 | 20 | 30 |
· Design Review | · 设计复审(和同事审核设计文档) | 30 | 50 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 100 | 200 |
· Coding | · 具体编码 | 200 | 400 |
· Code Review | · 代码复审 | 30 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 100 |
Reporting | 报告 | 30 | 50 |
· Test Repor | · 测试报告 | 30 | 20 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 15 |
· 合计 | 1120 | 1925 |
项目小结
我们的项目是实现一个自动生成小学四则运算题目的命令行程序,它可以根据用户的参数生成不同数量的题目,并且可以判断用户的答案是否正确。我们的项目主要分为四个模块,通过这四个主要模块可以比较好的完成题目的要求。
- 生成随机数模块
- 生成-表达式模块
- 生成题目和文件模块
- 统计题目对错模块
我们认为我们的项目是成功的,因为我们实现了所有的需求,并且通过了测试用例。我们也遇到了一些困难和挑战,例如如何保证题目不重复,如何保证生成的表达式是合法的,如何处理分数的运算和格式化(假分数与带分数的转换,如何设计合理的函数等。我们通过查阅资料,讨论方案,调试代码等方式解决了这些问题。
我们觉得结对编程是一种很好的学习方式,它可以让我们互相交流思想,互相帮助解决问题,互相监督进度,互相提高水平。我们在结对过程中也有很好的感受,例如相互信任,相互尊重,相互鼓励等,尤其在讨论时有更多更好的思路,而不仅局限于自己的想法。
对彼此结对中的闪光点或建议分享:
钟同学在项目中表现得很出色,有很强的逻辑思维能力和行动能力,能够快速理解需求和设计方案,并且能够高效地实现代码。陈同学也有很好的沟通能力和团队合作精神,能够主动提出意见和建议,并且能够耐心地听取我的想法和反馈,是一个很好的结对伙伴。