⭐成员:3223004473詹艺珏 and 3223004301吴梦琪
📎Github链接:https://github.com/Jue610/Jue610/tree/main/ArithProbelm
这个作业属于哪个课程 | 23软件工程 |
---|---|
这个作业要求在哪里 | 【作业3】结对项目 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序,培养团队协作和沟通交流能力 |
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 440 | 380 |
· Analysis | 需求分析 (包括学习新技术) | 60 | 30 |
· Design Spec | 生成设计文档 | 40 | 30 |
· Design Review | 设计复审 | 30 | 10 |
· Coding Standard | 代码规范 (制定开发规范) | 10 | 10 |
· Design | 具体设计 | 30 | 40 |
· Coding | 具体编码 | 180 | 120 |
· Code Review | 代码复审 | 30 | 60 |
· Test | 测试(自我测试,修改代码,提交修改) | 60 | 80 |
Reporting | 报告 | 130 | 160 |
· Test Report | 测试报告 | 90 | 120 |
· Size Measurement | 计算工作量 | 10 | 10 |
· Postmortem | 事后总结, 并提出过程改进计划 | 30 | 30 |
Total | 合计 | 600 | 570 |
一、程序功能
- 目的
- 主要用于生成数学表达式的练习题并计算答案,同时也能检查用户对这些练习题答案的正确性。
- 它可以随机生成包含自然数、分数和带分数的四则运算表达式,将其规范化后写入练习题文件,计算答案并写入答案文件,最后根据用户提供的答案文件检查答案的正确性并生成成绩文件。
- 输入输出
- 输入:通过命令行参数接受输入:
- 生成练习题的数量
-n
和表达式中数字的范围-r
来生成练习题;
python main.py -n 10000 -r 100
- 练习题文件
-e
和答案文件-a
来检查答案。
python main.py -e Exercises.txt -a Answers.txt
- 输出:生成练习题文件
Exercises.txt
、答案文件Answers.txt
以及成绩文件Grade.txt
。
二、代码结构与模块分析
- 类的定义
Node
类- 功能:用于构建表达式树。
- 结构:
- 有
type
属性,表示节点类型(如number
或operator
)。 value
属性,当type
为number
时存储分数值(以元组形式表示分子分母)。operator
属性,当type
为operator
时存储操作符(如+
、-
、×
、÷
)。left
和right
属性,用于指向表达式树的左子树和右子树。
- 有
class Node:def __init__(self, type, value=None, operator=None, left=None, right=None):self.type = typeself.value = valueself.operator = operatorself.left = leftself.right = right
- 函数模块与接口设置
simplify_fraction
函数- 功能:通过计算最大公约数来化简分数。
- 使用递归的方式计算分子分母的最大公约数,然后将分数化简。
def simplify_fraction(numerator, denominator):gcd = lambda a, b: a if b == 0 else gcd(b, a % b)common = gcd(abs(numerator), abs(denominator))return (numerator // common, denominator // common)
generate_number
函数- 功能:随机生成自然数、分数或带分数。
- 首先随机选择一种数字类型(自然数、分数、带分数),然后根据类型生成相应的数字,并将其包装成
Node
类型的对象。
def generate_number(r):choice = random.choices(['natural', 'fraction', 'mixed'], weights=[3, 2, 2], k=1)[0]if choice == 'natural':num = random.randint(0, r - 1)return Node('number', value=simplify_fraction(num, 1))elif choice == 'fraction':denominator = random.randint(2, r)numerator = random.randint(1, denominator - 1)return Node('number', value=simplify_fraction(numerator, denominator))else:integer = random.randint(1, r - 1)denominator = random.randint(2, r)numerator = random.randint(1, denominator - 1)total_numerator = integer * denominator + numeratorreturn Node('number', value=simplify_fraction(total_numerator, denominator))
generate_expression
函数- 功能:递归生成四则运算表达式树。
- 如果最大操作符数量为0,则生成一个数字节点;否则选择一个操作符,递归生成左右子表达式树,最后构建一个操作符节点并返回。
def generate_expression(r, max_ops):if max_ops == 0:return generate_number(r)else:op = random.choice(['+', '-', '×', '÷'])left_ops = random.randint(0, max_ops - 1)right_ops = max_ops - 1 - left_opsleft = generate_expression(r, left_ops)right = generate_expression(r, right_ops)return Node('operator', operator=op, left=left, right=right)
compute_value
函数- 功能:计算表达式树的值。
- 如果是数字节点直接返回其值;否则先计算左右子树的值,然后根据操作符进行相应的四则运算,并化简结果。
def compute_value(node):if node.type == 'number':return node.valueleft = compute_value(node.left)right = compute_value(node.right)ln, ld = leftrn, rd = rightif node.operator == '+':numerator = ln * rd + rn * lddenominator = ld * rdelif node.operator == '-':numerator = ln * rd - rn * lddenominator = ld * rdif numerator < 0:raise ValueError("Negative result")elif node.operator == '×':numerator = ln * rndenominator = ld * rdelif node.operator == '÷':if rn == 0:raise ValueError("Division by zero")numerator = ln * rddenominator = ld * rnif numerator / denominator >= 1:raise ValueError("Improper fraction")return simplify_fraction(numerator, denominator)
fraction_to_string
函数- 功能:将分数转换为字符串形式。
- 根据分子分母的关系判断并构建相应的字符串形式。
def fraction_to_string(numerator, denominator):if denominator == 1:return str(numerator)integer = numerator // denominatorremainder = numerator % denominatorif integer == 0:return f"{remainder}/{denominator}"else:return f"{integer}'{remainder}/{denominator}" if remainder != 0 else str(integer)
to_string
函数- 功能:将表达式树转换为字符串形式。
- 如果是数字节点,调用
fraction_to_string
函数转换为字符串;否则根据操作符的优先级和括号的需求,递归地将左右子树转换为字符串并构建表达式字符串。
def to_string(node, parent_priority=0):if node.type == 'number':return fraction_to_string(*node.value)current_priority = 2 if node.operator in ['×', '÷'] else 1left_str = to_string(node.left, current_priority)right_str = to_string(node.right, current_priority)expr_str = f"{left_str} {node.operator} {right_str}"if current_priority < parent_priority:expr_str = f"({expr_str})"return expr_str
normalize
函数- 功能:对表达式树进行规范化处理。
- 如果是数字节点直接返回;对于加法和乘法操作符,将表达式树展开为项列表,排序后重新构建表达式树;对于减法和除法操作符,递归地规范化左右子树。
def normalize(node):if node.type == 'number':return nodeif node.operator in ['+', '×']:terms = []stack = [node]while stack:current = stack.pop()if current.type == 'operator' and current.operator == node.operator:stack.append(current.right)stack.append(current.left)else:terms.append(current)terms = [normalize(term) for term in terms]terms.sort(key=lambda x: to_string(x))root = terms[0]for term in terms[1:]:root = Node('operator', operator=node.operator, left=root, right=term)return rootelse:return Node('operator', operator=node.operator, left=normalize(node.left), right=normalize(node.right))
generate_problems
函数- 功能:生成指定数量的练习题并计算答案。
- 通过循环随机生成表达式,规范化后将表达式和答案分别写入练习题文件和答案文件。
def generate_problems(n, r):generated = set()exercises = []answers = []while len(exercises) < n:expr = generate_expression(r, 3)normalized = normalize(expr)expr_str = to_string(normalized) + " ="if expr_str in generated:continuetry:result = compute_value(expr)generated.add(expr_str)exercises.append(expr_str)answers.append(fraction_to_string(*result))except ValueError:pass# 写入题目文件with open('Exercises.txt', 'w') as f:for i, expr in enumerate(exercises, 1):f.write(f"{i}. {expr}\n")# 写入答案文件with open('Answers.txt', 'w') as f:for i, ans in enumerate(answers, 1):f.write(f"{i}. {ans}\n")
parse_answer
函数- 功能:将答案字符串解析为
Fraction
对象。 - 根据字符串中是否包含带分数的标识进行不同的解析操作。
- 功能:将答案字符串解析为
def parse_answer(answer_str):answer_str = answer_str.strip()if "'" in answer_str:mixed, frac = answer_str.split("'", 1)whole = int(mixed)numerator, denominator = frac.split('/')return Fraction(whole * int(denominator) + int(numerator), int(denominator))elif '/' in answer_str:numerator, denominator = answer_str.split('/')return Fraction(int(numerator), int(denominator))else:return Fraction(int(answer_str), 1)
parse_expression
函数- 功能:将表达式字符串解析为可用于
eval
函数计算的Python表达式字符串。 - 进行一些字符替换(如将
×
替换为*
,÷
替换为/
),然后转换为相应的Python表达式字符串。
- 功能:将表达式字符串解析为可用于
def parse_expression(expr_str):expr_str = expr_str.replace(' ', '').replace('×', '*').replace('÷', '/')tokens = re.findall(r"(\d+'\d+/\d+|\d+/\d+|\d+|\+|\-|\*|/|\(|\))", expr_str)converted = []for token in tokens:if token in '()+-*/':converted.append(token)else:converted.append(convert_number(token))return ''.join(converted)
convert_number
函数- 功能:将数字字符串转换为
Fraction
对象的字符串表示形式。 - 根据字符串是否为带分数或普通分数进行不同的转换操作。
- 功能:将数字字符串转换为
def convert_number(s):if "'" in s:mixed, frac = s.split("'", 1)numerator, denominator = frac.split('/')return f"Fraction({int(mixed) * int(denominator) + int(numerator)}, {denominator})"elif '/' in s:numerator, denominator = s.split('/')return f"Fraction({numerator}, {denominator})"else:return f"Fraction({s}, 1)"
evaluate_expression
函数- 功能:计算解析后的表达式字符串的值。
- 使用
eval
函数在给定的Fraction
对象的命名空间下计算表达式的值。
def evaluate_expression(py_expr):try:return eval(py_expr, {'Fraction': Fraction})except:return None
check_answers
函数- 功能:检查答案文件中的答案是否正确。
- 读取练习题文件和答案文件,对每个练习题解析表达式计算正确答案,解析用户答案,然后比较两者是否相等,最后将正确和错误的题目编号分别写入成绩文件。
def check_answers(exercise_file, answer_file):correct = []wrong = []# 读取题目文件with open(exercise_file, 'r') as f:exercises = [line.strip().split('. ', 1)[1].rstrip(' =') for line in f] # 提取 "题目内容"# 读取答案文件with open(answer_file, 'r') as f:answers = [line.strip().split('. ', 1)[1] for line in f] # 提取 "答案内容"if len(exercises) != len(answers):print("错误:题目与答案数量不匹配!")returnfor idx in range(len(exercises)):expr = exercises[idx]user_ans = answers[idx]# 解析表达式并计算正确答案py_expr = parse_expression(expr)correct_ans = evaluate_expression(py_expr)if correct_ans is None:wrong.append(idx + 1)continue# 解析答案try:user_frac = parse_answer(user_ans)except:wrong.append(idx + 1)continue# 比较答案if correct_ans == user_frac:correct.append(idx + 1)else:wrong.append(idx + 1)# 输出结果with open('Grade.txt', 'w') as f:f.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")f.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")
main
函数- 功能:解析命令行参数并调用相应的函数(generate_problems或check_answers)。
- 使用argparse模块解析命令行参数,根据参数调用不同的函数,如果参数不完整则输出错误信息并退出程序。
def main():parser = argparse.ArgumentParser()group = parser.add_mutually_exclusive_group(required=True)group.add_argument("-n", type=int)group.add_argument("-e", type=str)parser.add_argument("-r", type=int)parser.add_argument("-a", type=str)args = parser.parse_args()if args.n is not None:if args.r is None:print("Error: -r required")sys.exit(1)generate_problems(args.n, args.r)elif args.e is not None:if args.a is None:print("Error: -a required")sys.exit(1)check_answers(args.e, args.a)
- 流程图
三、性能分析
对于生成模式的性能分析:
对于判断模式的性能分析:
可以看出,生成题目的函数消耗最大(因为分析时让程序生成10000道题目)。
四、单元测试用例
- 主要分为
TestMathGenerator
和TestCLICommands
两个测试用例类。 - 单元测试覆盖率:
TestMathGenerator
类分析setUp
和tearDown
方法- 在
setUp
中,创建临时目录用于测试文件的操作,备份了原始的命令行参数和标准输出。 tearDown
方法用于清理临时目录、恢复原始的命令行参数和标准输出,并删除测试过程中可能生成的输出文件。
- 在
test_number_generation
方法- 设置随机种子为
42
,调用generate_number
函数生成一个数,然后对生成的数的格式进行验证,确保其是合法的分数形式。
- 设置随机种子为
test_expression_calculation
方法- 针对表达式计算函数
compute_value
进行测试。对加除法乘法三种运算分别构建测试表达式,验证计算结果是否符合预期。
- 针对表达式计算函数
test_normalization
方法- 构建一个复杂的表达式,调用
normalize
函数对表达式进行规范化,然后使用to_string
函数将规范化后的表达式转换为字符串,验证转换后的字符串是否符合预期形式。
- 构建一个复杂的表达式,调用
test_file_generation
方法- 通过模拟命令行参数(
-n
和-r
)调用main
函数,然后验证是否成功生成了Exercises.txt
和Answers.txt
文件,并且文件中的行数是否与预期的数量一致。
- 通过模拟命令行参数(
test_correct_answers
方法- 创建测试用的题目文件和答案文件内容,调用
create_test_files
函数创建临时的测试文件,再调用check_answers
函数对答案进行检查。最后验证Grade.txt
文件是否存在,并检查文件内容中正确答案和错误答案的统计是否符合预期。
- 创建测试用的题目文件和答案文件内容,调用
test_mixed_answers
方法- 创建包含混合正确/错误答案的题目文件和答案文件内容,创建临时文件并调用
check_answers
函数。最后验证Grade.txt
文件中的正确答案和错误答案统计符合预期。
- 创建包含混合正确/错误答案的题目文件和答案文件内容,创建临时文件并调用
test_cli_error_handling
方法- 测试命令行缺少必要参数时的错误处理。
TestCLICommands
类分析setUp
和tearDown
方法- 准备测试环境和清理测试后的环境。
test_generate_command
方法- 通过模拟命令行参数(
-n
和-r
)调用main
函数,验证是否成功生成了Exercises.txt
文件,并且文件中的行数是否与-n
指定的数量一致,以及每行是否包含=
符号。
- 通过模拟命令行参数(
test_check_command
方法- 先准备测试用的题目文件和答案文件,然后通过模拟命令行参数(
-e
和-a
)调用main
函数,验证是否成功生成了Grade.txt
文件,并且文件内容中正确答案的统计是否符合预期。
- 先准备测试用的题目文件和答案文件,然后通过模拟命令行参数(
test_simple_generation
方法- 创建一个测试文件,验证文件是否存在后将其删除。
test_basic_answer_check
方法- 创建测试用的题目文件和答案文件,然后调用
check_answers
函数进行答案核对,最后验证是否成功生成了Grade.txt
文件。
- 创建测试用的题目文件和答案文件,然后调用
五、总结与反思
成功之处
- 功能实现方面
实现了基本需求,包括按照指定数量和数值范围生成四则运算题目、确保题目计算过程中不产生负数、除法结果为真分数、题目不重复、生成答案文件、对给定的题目和答案文件进行对错判定等功能。 - 测试覆盖方面
编写了多个测试用例,覆盖了程序的各种功能和边界情况,有效地保证了程序的正确性。
不足之处
- 性能优化方面
虽然进行了一定的性能优化,但在生成大量题目时,程序的运行速度还有提升的空间。 - 异常处理方面
在程序中,部分异常情况的处理还不够完善。比如在处理文件读写异常时,只是简单地给出了一些基本的错误提示,没有更详细的错误信息来帮助用户定位问题。
结对感受
- 闪光点
💙 我们可以同时开展不同的任务:一个人负责设计和编写主要的逻辑代码,另一个人可以同时进行相关功能的测试用例编写。
💚两个人可以及时互相审查代码。当一个人完成了一部分代码编写后,另一个人可以迅速进行审查,发现潜在的逻辑错误、代码规范问题或者性能瓶颈等。
😎 我们通过互相学习,不仅提高了自己的技能,还增进了对整个项目的理解,并且可以互相监督对方的进度。 - 对彼此的看法
😄zyj:我的搭档在项目开发的时候充满热情。即使在遇到困难和压力的时候,她也能保持积极的态度,这种热情也感染着我。
😊 wmq:在这次的作业合作中,我深刻体会到我的搭档的出色之处,她思维缜密,逻辑清晰有条理,且对待工作细致严谨,真是难能可贵的合作伙伴! - 建议
在结对过程中,沟通的效率还有待提高。在今后的结对项目中,我们应该更加明确地表达自己的想法,并且在讨论之前先对问题进行更深入的思考,以提高沟通的效率。