基于python的结对作业
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230 |
团队成员1 | 何昌洲 3122004737 |
团队成员2 | 郑玮源 3122004760 |
这个作业的目标 | 用python语言结对合作完成小学四则运算题目的程序,提高项目合作能力 |
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 25 |
Estimate | 估计这个任务需要多少时间 | 30 | 35 |
Development | 开发 | 300 | 400 |
Analysis | 需求分析 (包括学习新技术) | 120 | 150 |
Design Spec | 生成设计文档 | 30 | 30 |
Design Review | 设计复审 | 30 | 35 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 20 |
Design | 具体设计 | 60 | 60 |
Coding | 具体编码 | 70 | 120 |
Code Review | 代码复审 | 20 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 20 | 60 |
Test Repor | 测试报告 | 20 | 20 |
Size Measurement | 计算工作量 | 10 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 10 |
合计 | 800 | 855 |
二、运行环境
IDE:PyCharm 2024.1.4
三、核心函数设计
流程图
一、程序分析
程序实现了以下主要功能:
1.带分数处理函数
- mixed_to_fraction(mixed_str): 这个函数将带分数(如1'1/2)或普通分数(如3/4)转换为 fractions.Fraction 对象,以方便后续的精确计算。
- fraction_to_mixed(f): 将分数转换为带分数的格式(如 7/3 转换为 2'1/3),以便结果能以更友好的形式呈现。
2.生成随机数和真分数
- generate_number(range_limit): 这个函数根据给定的范围生成随机的自然数或分数。为了避免分母为零的情况,特别地在生成分数时,确保分母不为0,且分子和分母的组合不整除,以保证生成的是真分数。
3.表达式计算
- eval_expression(expr): 通过将输入表达式中的带分数和普通分数字符串转换为 Fraction 对象,然后使用 eval 函数来计算表达式的值。表达式中的除法使用÷符号,eval 会将其替换为标准的除法运算 /。对于除零操作,程序会返回一个特殊值 float('inf') 以表示错误。
4.生成子运算式
- generate_subexpression(range_limit): 根据传入的数值范围,生成一个包含1到3个数的运算子表达式,可能包含分数和自然数。为了保证表达式不会生成负数,程序会在必要时将减法运算替换为加法运算,或重新生成运算数。
5.生成完整的表达式
- generate_expression(range_limit): 这个函数生成一个由两个子运算式组成的完整四则运算表达式。程序确保运算表达式不会出现负数,并且在除法时避免除零操作。
6.生成题目和答案
- generate_quiz(num_questions, range_limit): 根据传入的题目数量和数值范围,生成题目和答案。题目会被写入到 Exercises.txt 文件,答案会被保存到 Answers.txt 文件中。
7.批改答案
- grade(exercise_file, answer_file): 读取题目和答案文件,逐一计算题目中的表达式并与答案文件中的答案进行对比。结果会分为正确和错误两个列表,并输出到 Grade.txt 文件中。
二、主函数模块
主函数使用 argparse 模块解析命令行参数。它支持以下几种命令:
- -n 和 -r: 用于生成题目,-n 指定题目数量,-r 指定题目中数值的范围。例如:-n 10 -r 100 表示生成 10 道题目,数值范围在 0 到 100 之间。
- -e 和 -a: 用于批改题目,-e 指定题目文件,-a 指定答案文件。
根据传入的参数,程序会调用 generate_quiz 或 grade 函数来完成相应的任务。
三、设计优点
1.精确处理分数:通过 fractions.Fraction 确保分数运算准确,避免浮点误差,支持带分数格式转换。
2.题目生成灵活:随机生成自然数和分数,支持四则运算,避免负数和除零问题。
3.用户友好:命令行参数便于使用,生成题目和答案分别保存到文件,批改结果清晰呈现。
4.自动批改功能:精确对比答案与计算结果,正确和错误题目分开记录。
5.模块化设计:各功能独立,易于维护和扩展,支持性能分析。
6.智能表达式处理:通过正则表达式处理输入的带分数和分数形式,增加灵活性。
四、主要函数设计代码
1.带分数处理
# 将带分数的字符串转换为 Fraction
def mixed_to_fraction(mixed_str):
if "'" in mixed_str:
whole, frac = mixed_str.split("'")
whole = int(whole)
numerator, denominator = map(int, frac.split('/'))
return fractions.Fraction(whole * denominator + numerator, denominator)
elif "/" in mixed_str:
numerator, denominator = map(int, mixed_str.split('/'))
return fractions.Fraction(numerator, denominator)
else:
return fractions.Fraction(int(mixed_str), 1)
# 将分数转换为带分数的字符串
def fraction_to_mixed(f):
if f.denominator == 1:
return str(f.numerator)
elif f.numerator > f.denominator:
whole = f.numerator // f.denominator
remainder = f.numerator % f.denominator
return f"{whole}'{remainder}/{f.denominator}" if remainder != 0 else str(whole)
return str(f)
2.表达式的生成和计算
def eval_expression(expr):
# 替换带分数为正确的 Fraction 表示
expr = re.sub(r"(\d+)'(\d+)/(\d+)", lambda m: str(mixed_to_fraction(m.group(0))), expr)
# 将分数转换为 Fraction 对象
expr = re.sub(r"(\d+)/(\d+)", lambda m: str(fractions.Fraction(int(m.group(1)), int(m.group(2)))), expr)
# 将 ÷ 替换为 /,使用 Fraction 处理除法
expr = expr.replace('÷', '/')
try:
# 使用 eval 计算表达式,确保使用 fractions.Fraction 进行计算,避免浮点精度问题
return eval(expr, {"__builtins__": None}, {"Fraction": fractions.Fraction})
except ZeroDivisionError:
return float('inf') # 返回一个特殊值,表示出现了除以0的情况
# 生成子运算式,包含 1 到 3 个运算数,避免负数,考虑优先级
def generate_subexpression(range_limit):
num_count = random.randint(1, 3) # 随机生成1到3个运算数
numbers = [generate_number(range_limit) for _ in range(num_count)]
operators = ['+', '-', '*', '÷']
# 当产生的单运算数是分数时,加上括号,避免出错
if num_count == 1:
if numbers[0].denominator == 1:
return fraction_to_mixed(numbers[0]) # 如果只有一个运算数,不需要括号
else:
return f"({fraction_to_mixed(numbers[0])})"
subexpression = fraction_to_mixed(numbers[0])
for i in range(1, num_count):
op = random.choice(operators)
# 避免除0
if op == '÷' and numbers[i] == 0:
numbers[i] = generate_number(range_limit)
# 避免出现负数,出现时转换为加法
if op == '-' and numbers[i] > numbers[i - 1]:
op = '+'
subexpression += f" {op} {fraction_to_mixed(numbers[i])}"
if eval_expression(subexpression) < 0:
subexpression = subexpression.replace('-', '+')
# 如果有多个运算数,加上括号
return f"({subexpression})" if num_count > 1 else subexpression
# 生成完整的运算表达式,由 2 个子运算式组成,确保不会产生负数
def generate_expression(range_limit):
subexpression_count = 2
subexpressions = [generate_subexpression(range_limit) for _ in range(subexpression_count)]
operators = ['+', '-', '*', '÷']
# 避免最终表达式出现四个运算符
matches = []
for ch in subexpressions:
if ch in operators:
matches = matches.append(ch)
expression = subexpressions[0]
current_value = eval_expression(subexpressions[0]) # 计算当前表达式的值
for i in range(1, subexpression_count):
op = random.choice(operators)
# 记录
while len(matches) > 2 & (op not in matches):
op = random.choice(operators)
if op not in matches:
matches.append(op)
next_value = eval_expression(subexpressions[i])
# 确保不会产生负数,出现时转换为加法
if op == '-':
if current_value - next_value < 0:
op = '+'
current_value = current_value + next_value
else:
current_value = current_value - next_value
# 其他操作更新当前值
elif op == '+':
current_value = current_value + next_value
elif op == '*':
current_value = current_value * next_value
elif op == '÷':
while next_value == 0:
subexpressions[i] = generate_subexpression(range_limit) # 确保不除以0
next_value = eval_expression(subexpressions[i])
current_value = current_value / next_value
# 拼接表达式
expression += f" {op} {subexpressions[i]}"
return expression, current_value
3.生成题目和答案
# 生成题目和答案
def generate_quiz(num_questions, range_limit):
exercises = []
answers = []
for _ in range(num_questions):
expression, answer = generate_expression(range_limit)
while answer < 0:
expression, answer = generate_expression(range_limit)
exercises.append(expression)
answers.append(fraction_to_mixed(fractions.Fraction(answer).limit_denominator()))
with open("Exercises.txt", "w") as ex_file, open("Answers.txt", "w") as ans_file:
for i, (exercise, answer) in enumerate(zip(exercises, answers), 1):
ex_file.write(f"{i}. {exercise} = \n")
4.批改答案
# 批改答案
def grade(exercise_file, answer_file):
with open(exercise_file, "r") as ex_file, open(answer_file, "r") as ans_file:
exercises = ex_file.readlines()
answers = ans_file.readlines()
correct = []
wrong = []
for i, (exercise, answer) in enumerate(zip(exercises, answers), 1):
# 去除题目中的编号,获取表达式部分
expr = exercise.split('=')[0].strip() # 去除 = 号后面的内容
expr = re.sub(r'^\d+\.\s*', '', expr) # 去除题目编号 "1. " 等格式
# 将预期答案转换为标准 Fraction 形式
expected_answer = answer.split('.')[1].strip()
expected_fraction = mixed_to_fraction(expected_answer)
# 计算表达式的答案
calculated_fraction = fractions.Fraction(eval_expression(expr.replace('÷', '/'))).limit_denominator()
# 比较计算结果和预期答案,统一比较 Fraction 对象而不是字符串
if expected_fraction == calculated_fraction:
correct.append(i)
else:
wrong.append(i)
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(wrong)} ({', '.join(map(str, wrong))})\n")
四、性能分析
通过cprofile可以对整个程序的执行进行性能分析,找到耗时较多的部分,然后进一步优化。
main函数运行
cProfile.run("main()", filename="performance_analysis_result")
命令行运行
snakeviz.exe -p 8080 .\performance_analysis_result
得到性能分析图
1.题目生成
可见在批改函数中generate_expression运行时间最长,故可以考虑对函数generate_expression进行优化。
2.题目批改
可见在批改函数中eval_expression运行时间最长,故可以考虑对函数eval_expression进行优化。
五、单元测试
一、关于测试点的思维导图
二、主要测试代码
测试代码如下:
1.转换模块测试
# 测试混合分数转换
def test_mixed_to_fraction(self):
self.assertEqual(mixed_to_fraction("2'3/4"), fractions.Fraction(11, 4))
self.assertEqual(mixed_to_fraction("3/5"), fractions.Fraction(3, 5))
self.assertEqual(mixed_to_fraction("5"), fractions.Fraction(5, 1))
# 测试分数转换为混合分数字符串
def test_fraction_to_mixed(self):
self.assertEqual(fraction_to_mixed(fractions.Fraction(11, 4)), "2'3/4")
self.assertEqual(fraction_to_mixed(fractions.Fraction(3, 5)), "3/5")
self.assertEqual(fraction_to_mixed(fractions.Fraction(5, 1)), "5")
2.表达式生成模块测试
# 测试减法不出现负数
def test_no_negative_subtraction(self):
for _ in range(100): # 多次测试
result = generate_subexpression(10)
if '-' in result:
# 检查是否产生负数
eval_result = eval_expression(result.replace('÷', '/'))
self.assertTrue(eval_result >= 0, "表达式不应生成负数")
# 测试避免除以0
def test_avoid_zero_division(self):
for _ in range(100): # 多次测试
result = generate_subexpression(10)
self.assertNotIn("0", result) # 确保没有0作为除数
# 测试生成完整运算表达式
def test_generate_expression(self):
expression, answer = generate_expression(10)
self.assertTrue(isinstance(expression, str))
self.assertTrue(eval_expression(expression) >= 0)
3.计算模块测试
# 测试表达式求值
def test_eval_expression(self):
self.assertEqual(fractions.Fraction(eval_expression("(2'1/3 + 3/4)")).limit_denominator(),
fractions.Fraction(37, 12))
4.文件处理模块测试
# 测试生成题目到文件
def test_generate_quiz(self):
generate_quiz(5, 10)
with open("Exercises.txt", "r") as ex_file:
exercises = ex_file.readlines()
with open("Answers.txt", "r") as ans_file:
answers = ans_file.readlines()
self.assertEqual(len(exercises), 5)
self.assertEqual(len(answers), 5)
# 测试批改功能
def test_grade(self):
generate_quiz(5, 10)
generate_quiz(5, 10) # 生成一套题目以进行批改
grade("Exercises.txt", "Answers.txt")
with open("Grade.txt", "r") as grade_file:
results = grade_file.read()
self.assertIn("Correct:", results)
self.assertIn("Wrong:", results)
5.参数处理模块测试
# 测试命令行参数处理错误情况
def test_main_args_error(self):
# 使用 mock 来测试错误情况
with self.assertRaises(SystemExit):
sys.argv = ["script_name"]
main()
6.边界条件测试
def test_generate_number_boundary(self):
for _ in range(100):
num = generate_number(1000)
self.assertNotEqual(num.denominator, 0)
三、测试代码覆盖率
四、测试结果
六、项目总结
一、项目分工
郑玮源:负责大部分主函数代码和测试的编写
何昌洲:负责小部分代码补充和博客的编写
二、项目启发
在参与编程结对项目的过程中,我从技术层面和团队合作层面都获得了许多宝贵的经验与启发:
1.技术层面
- 代码质量提升:结对编程的核心理念是两名开发者共同编写代码,一名编写代码,另一名审阅和提出建议。这种模式让我在编码时更加注重代码的清晰度、简洁性和可读性。通过互相审查代码,能够及时发现并修复潜在的代码问题,减少了低级错误的出现。
- 最佳实践的学习:通过与结对伙伴的合作,我能够学习到他们的编程风格、常用的设计模式和工具链。这些在实际项目中可以快速应用,扩展了我的技术视野,帮助我在面对不同问题时有了更多的解决思路。
- 快速调试与问题解决:与搭档一起调试程序时,我们能够在不同的思路下更快定位和解决问题。许多原本单人编程时会忽略的细节,通过结对时的讨论得以被及时发现,极大提高了调试效率。
- 代码重构与优化:通过持续的代码审查和讨论,我们不仅能完成项目功能,还能对代码进行优化和重构,使其更具可维护性和扩展性。每当遇到复杂的逻辑,我们会互相探讨最优的设计方案,并进行合理的拆分和简化。
- 工具链和开发流程:在项目中,我熟悉了版本控制工具Git和协作平台Github的使用,尤其是在代码合并、冲突解决和持续集成方面。结对项目的协作流程让我对这些工具的功能和用法有了更深入的理解。
2.团队合作层面
- 沟通与反馈的有效性:结对编程需要双方在编码过程中持续保持沟通,及时给予反馈。通过这一过程,我学会了如何有效传达我的想法,并在听取他人建议时能够快速理解并回应。这种高效的沟通模式不仅在编程中受益,也为我今后在团队中的协作提供了很好的范例。
- 责任感与共同目标:结对编程让我更深刻地体会到团队合作中的责任感。当我们一起面对项目中的难题时,我不再是单独为自己负责,而是为整个团队的目标负责。这种责任感促使我在项目的每个阶段都保持高效和专注,推动项目顺利进行。
- 共享知识与经验:在合作的过程中,我们不仅是在完成项目,还在共享彼此的知识和经验。通过共同讨论和解决问题,我能够吸收到更多有用的经验和知识,同时我也乐于分享我所掌握的技术,这种双向的学习模式极大提升了团队的整体技术水平。