【作业3】结对项目:实现一个自动生成小学四则运算题目的命令行程序
⭐成员: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 = type
self.value = value
self.operator = operator
self.left = left
self.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 + numerator
return 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_ops
left = 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.value
left = compute_value(node.left)
right = compute_value(node.right)
ln, ld = left
rn, rd = right
if node.operator == '+':
numerator = ln * rd + rn * ld
denominator = ld * rd
elif node.operator == '-':
numerator = ln * rd - rn * ld
denominator = ld * rd
if numerator < 0:
raise ValueError("Negative result")
elif node.operator == '×':
numerator = ln * rn
denominator = ld * rd
elif node.operator == '÷':
if rn == 0:
raise ValueError("Division by zero")
numerator = ln * rd
denominator = ld * rn
if 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 // denominator
remainder = numerator % denominator
if 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 1
left_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 node
if 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 root
else:
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:
continue
try:
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("错误:题目与答案数量不匹配!")
return
for 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:在这次的作业合作中,我深刻体会到我的搭档的出色之处,她思维缜密,逻辑清晰有条理,且对待工作细致严谨,真是难能可贵的合作伙伴! - 建议
在结对过程中,沟通的效率还有待提高。在今后的结对项目中,我们应该更加明确地表达自己的想法,并且在讨论之前先对问题进行更深入的思考,以提高沟通的效率。


浙公网安备 33010602011771号