结对项目

所属课程 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34
作业要求 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13230
作业目标 按照要求合作实现一个自动生成小学四则运算题目的命令行程序
成员1 于海洋3122004758
成员2 钟启腾3122004761

Github链接: https://github.com/hhai12345678/3122004758

1. PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 40
· Estimate · 估计这个任务需要多少时间 20 20
Development 开发 300 350
· Analysis · 需求分析 (包括学习新技术) 60 80
· Design Spec · 生成设计文档 50 50
· Design Review · 设计复审 20 30
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 20
· Design · 具体设计 60 80
· Coding · 具体编码 100 120
· Code Review · 代码复审 30 30
· Test · 测试(自我测试,修改代码,提交修改) 60 80
Reporting 报告 60 60
· Test Repor · 测试报告 30 40
· Size Measurement · 计算工作量 20 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 30
· 合计 890 1050

2. 效能分析

3. 设计实现过程

3.1 要求分析

  • 输入参数:程序需要支持命令行参数 -n、-r、-e 和 -a。其中 -n 表示生成的题目数量,-r 表示数字范围,-e 和 -a 用于验证答案。

  • 题目生成

    • 数值范围受 -r 控制,支持自然数和真分数。
    • 题目中不能有负数结果,除法结果必须是分数。
    • 运算符最多出现 3 次。
  • 答案验证

    • 通过对比生成的答案文件与用户提供的答案文件,输出正确与错误的题目编号。

3. 2 实现步骤

  1. 处理命令行参数

  2. 题目生成

    • 随机生成操作数和运算符。

    • 检查运算符顺序是否符合规则。

    • 检查结果是否合法。

  3. 计算答案

  4. 文件输出

    • 题目输出到 Exercises.txt。

    • 答案输出到 Answers.txt。

  5. 答案验证

    • 读取用户提供的题目和答案文件。

    • 逐一对比,生成统计结果 Grade.txt。

3.3 使用方法

生成 10 个数值范围为 10 的题目:

python main.py -n 10 -r 10

验证题目和答案文件:

python main.py -e Exercises.txt -a Answers.txt

3.4 关键函数流程图

main.py

generate_expression(max_range, max_operators)

calculate_expression

4. 代码说明

4.1 代码实现

Main.py代码

import argparse
import random
import fractions

# 定义生成题目时使用的四种基本运算符
OPERATORS = ['+', '-', '*', '/']

# 生成一个分数,分母的最大值由max_denominator参数决定
def generate_fraction(max_denominator):
    denominator = random.randint(2, max_denominator)  # 随机生成分母,避免分母为1
    numerator = random.randint(1, denominator - 1)  # 随机生成分子,确保分子小于分母
    return fractions.Fraction(numerator, denominator)  # 返回一个分数对象

# 生成一个操作数,可以是整数或者分数
def generate_operand(max_range):
    if random.choice([True, False]):  # 随机选择生成整数或分数
        return str(random.randint(1, max_range - 1))  # 返回随机整数,避免整数为0
    else:
        fraction = generate_fraction(max_range)  # 生成一个分数
        return f"{fraction.numerator}/{fraction.denominator}"  # 返回分数形式的字符串

# 生成一个带有随机操作符的算术表达式,表达式中包含多个操作数和运算符
def generate_expression(max_range, max_operators):
    num_operators = random.randint(1, max_operators)  # 随机生成表达式中运算符的个数
    expression = generate_operand(max_range)  # 生成第一个操作数
    for _ in range(num_operators):
        operator = random.choice(OPERATORS)  # 随机选择一个运算符
        operand = generate_operand(max_range)  # 生成下一个操作数
        expression += f" {operator} {operand}"  # 将运算符和操作数添加到表达式中
    return expression  # 返回完整的表达式字符串

# 计算一个表达式的结果
def calculate_expression(expression):
    try:
        # 通过将字符串分割成单词来解析表达式,假设表达式格式正确
        tokens = expression.split()
        result = fractions.Fraction(tokens[0])  # 将第一个操作数转换为分数类型

        # 遍历剩余的运算符和操作数,并按顺序进行计算
        for i in range(1, len(tokens), 2):
            operator = tokens[i]  # 当前的运算符
            operand = fractions.Fraction(tokens[i + 1])  # 当前的操作数(以分数形式表示)

            # 根据运算符执行相应的操作
            if operator == '+':
                result += operand
            elif operator == '-':
                result -= operand
            elif operator == '*':
                result *= operand
            elif operator == '/':
                result /= operand  # 确保除法的结果是分数形式
        return result  # 返回计算结果,类型为Fraction
    except ZeroDivisionError:  # 捕捉除以零的情况
        return None  # 如果发生除以零,返回None表示无效的结果

# 生成指定数量的算术题目,并计算其答案
def generate_exercise(num_exercises, max_range, max_operators):
    exercises = []  # 存储生成的算术题目
    answers = []  # 存储对应的答案
    while len(exercises) < num_exercises:  # 循环直到生成所需数量的题目
        expression = generate_expression(max_range, max_operators)  # 生成一个题目
        result = calculate_expression(expression)  # 计算其答案
        if result is not None:  # 确保没有除以零的无效结果
            exercises.append(expression)  # 将题目添加到列表
            # 将答案转换为字符串形式,如果是整数则去掉分母
            answers.append(
                f"{result.numerator}/{result.denominator}" if result.denominator != 1 else str(result.numerator))
    return exercises, answers  # 返回题目和答案的列表

# 将题目或答案写入文件,每行一个
def write_to_file(filename, lines):
    with open(filename, 'w', encoding='utf-8') as file:  # 以写模式打开文件
        for line in lines:
            file.write(line + '\n')  # 每行写入一个字符串,并换行

# 从文件中读取内容,返回每行组成的列表
def read_file(filename):
    with open(filename, 'r', encoding='utf-8') as file:  # 以读模式打开文件
        return [line.strip() for line in file]  # 读取所有行,并去掉每行末尾的换行符

# 检查答案的正确性,将正确和错误的题目编号分别存储
def check_answers(exercise_file, answer_file):
    exercises = read_file(exercise_file)  # 从文件读取题目
    answers = read_file(answer_file)  # 从文件读取答案
    correct_indices = []  # 存储正确答案的题目编号
    wrong_indices = []  # 存储错误答案的题目编号

    # 遍历每个题目及其对应的用户答案
    for index, (exercise, user_answer) in enumerate(zip(exercises, answers), start=1):
        correct_answer = calculate_expression(exercise)  # 计算题目的正确答案
        correct_answer_str = f"{correct_answer.numerator}/{correct_answer.denominator}" if correct_answer.denominator != 1 else str(
            correct_answer.numerator)  # 将正确答案转换为字符串
        if user_answer == correct_answer_str:  # 检查用户答案是否正确
            correct_indices.append(index)  # 如果正确,记录题目编号
        else:
            wrong_indices.append(index)  # 如果错误,记录题目编号

    return correct_indices, wrong_indices  # 返回正确和错误的题目编号

# 将成绩(正确和错误的题目编号)写入文件
def write_grades(correct, wrong):
    with open('Grade.txt', 'w', encoding='utf-8') as file:  # 以写模式打开成绩文件
        file.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")  # 写入正确的题目数量和编号
        file.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")  # 写入错误的题目数量和编号

# 程序主函数,处理命令行参数
def main():
    parser = argparse.ArgumentParser(description="Generate and check elementary arithmetic problems.")  # 创建参数解析器
    parser.add_argument('-n', type=int, help="Number of exercises to generate.")  # 生成题目的数量
    parser.add_argument('-r', type=int, help="Range of numbers in exercises.")  # 操作数的范围
    parser.add_argument('-e', type=str, help="Exercise file to check answers.")  # 用于检查的题目文件
    parser.add_argument('-a', type=str, help="Answer file to check answers.")  # 用于检查的答案文件

    args = parser.parse_args()  # 解析命令行参数

    # 如果提供了生成题目所需的参数,生成题目并保存到文件
    if args.n and args.r:
        exercises, answers = generate_exercise(args.n, args.r, 3)  # 生成题目和答案,最大操作符数量为3
        write_to_file('Exercises.txt', exercises)  # 将题目写入文件
        write_to_file('Answers.txt', answers)  # 将答案写入文件
        print(f"Generated {args.n} exercises with answers.")  # 打印生成题目的信息
    # 如果提供了检查答案的文件,检查答案并生成成绩
    elif args.e and args.a:
        correct, wrong = check_answers(args.e, args.a)  # 检查答案
        write_grades(correct, wrong)  # 写入成绩
        print("Checked answers and wrote grades to Grade.txt.")  # 打印检查完成的信息
    else:
        parser.print_help()  # 如果参数不完整,打印帮助信息

# 运行主函数
if __name__ == "__main__":
    main()

测试单元代码

import random
import unittest
import fractions
from io import StringIO
import sys
from main import (generate_fraction, generate_operand, generate_expression,
                  calculate_expression, generate_exercise, write_to_file, read_file,
                  check_answers)


class TestMathExercise(unittest.TestCase):
    """
    TestMathExercise类用于测试与数学练习生成相关的函数。
    """

    def test_generate_fraction(self):
        """
        测试生成分数函数 generate_fraction(n) 是否返回合法的 fractions.Fraction 实例。
        分子应当大于等于 1,分母应当大于等于 2。
        """
        fraction = generate_fraction(10)
        self.assertTrue(isinstance(fraction, fractions.Fraction))  # 检查生成结果是否为 Fraction 实例
        self.assertGreaterEqual(fraction.denominator, 2)  # 检查分母是否符合条件
        self.assertGreaterEqual(fraction.numerator, 1)  # 检查分子是否符合条件

    def test_generate_expression(self):
        """
        测试生成表达式函数 generate_expression(max_value, num_operands)。
        确保生成的表达式中包含加减乘除运算符。
        使用固定随机种子以保证测试结果一致。
        """
        random.seed(0)  # 固定随机种子以保证测试的可重复性
        expression = generate_expression(10, 3)
        # 确保表达式中至少包含一个四则运算符号
        self.assertTrue(any(op in expression for op in ['+', '-', '*', '/']))

    def test_calculate_expression_addition(self):
        """
        测试计算表达式的函数 calculate_expression(expression) 对加法表达式的处理。
        例如: "1/2 + 1/2" 应该返回 1。
        """
        expression = "1/2 + 1/2"
        result = calculate_expression(expression)
        self.assertEqual(result, fractions.Fraction(1, 1))  # 检查结果是否为1

    def test_calculate_expression_subtraction(self):
        """
        测试 calculate_expression(expression) 对减法表达式的处理。
        例如:"3/4 - 1/4" 应该返回 1/2。
        """
        expression = "3/4 - 1/4"
        result = calculate_expression(expression)
        self.assertEqual(result, fractions.Fraction(1, 2))  # 检查结果是否为1/2

    def test_calculate_expression_multiplication(self):
        """
        测试 calculate_expression(expression) 对乘法表达式的处理。
        例如:"2/3 * 3/4" 应该返回 1/2。
        """
        expression = "2/3 * 3/4"
        result = calculate_expression(expression)
        self.assertEqual(result, fractions.Fraction(1, 2))  # 检查结果是否为1/2

    def test_calculate_expression_division(self):
        """
        测试 calculate_expression(expression) 对除法表达式的处理。
        例如:"2/3 / 4/5" 应该返回 10/12 (即 5/6)。
        """
        expression = "2/3 / 4/5"
        result = calculate_expression(expression)
        self.assertEqual(result, fractions.Fraction(10, 12))  # 检查结果是否为10/12

    def test_generate_exercise(self):
        """
        测试生成练习题函数 generate_exercise(num_exercises, max_value, num_operands)。
        确保生成的练习题数目与答案数目都符合预期(即 num_exercises 个)。
        """
        exercises, answers = generate_exercise(5, 10, 3)
        self.assertEqual(len(exercises), 5)  # 检查生成的练习题数量是否正确
        self.assertEqual(len(answers), 5)  # 检查生成的答案数量是否正确

    def test_check_answers(self):
        """
        测试检查答案的函数 check_answers(exercise_file, answer_file)。
        这里通过创建包含四个算式的文件,然后检查答案是否全部正确。
        """
        exercises = ["1/2 + 1/2", "3/4 - 1/4", "2/3 * 3/4", "2/3 / 4/5"]
        answers = ["1", "1/2", "1/2", "5/6"]

        # 模拟写入“练习题”和“答案”文件
        with open('Exercises.txt', 'w') as f_ex:
            f_ex.write('\n'.join(exercises))

        with open('Answers.txt', 'w') as f_ans:
            f_ans.write('\n'.join(answers))

        # 调用 check_answers 函数检查答案
        correct, wrong = check_answers('Exercises.txt', 'Answers.txt')
        self.assertEqual(correct, [1, 2, 3, 4])  # 检查是否所有答案都正确
        self.assertEqual(wrong, [])  # 检查是否没有错误答案

    def test_write_to_file(self):
        """
        测试写文件函数 write_to_file(filename, data)。
        确保文件中写入的数据与原始数据相同。
        """
        data = ["1 + 1", "2 - 2", "3 * 3", "4 / 4"]
        write_to_file("TestOutput.txt", data)

        # 读取文件并验证内容是否正确
        read_back = read_file("TestOutput.txt")
        self.assertEqual(data, read_back)  # 检查文件内容是否与原始数据一致

    def test_read_file(self):
        """
        测试读文件函数 read_file(filename)。
        确保从文件中读取的数据与原始写入的数据相同。
        """
        data = ["5 + 5", "6 - 6"]
        write_to_file("TestRead.txt", data)
        read_back = read_file("TestRead.txt")
        self.assertEqual(data, read_back)  # 检查读取的数据是否正确


if __name__ == '__main__':
    unittest.main()

4.2 详细说明

  • generate_fraction: 生成随机真分数。

  • generate_operand: 生成随机操作数,可以是自然数或真分数。

  • generate_expression: 根据给定的操作符数量生成一个表达式。

  • calculate_expression: 计算生成的表达式的结果,如果有除以零等非法操作,返回 None。

  • generate_exercise: 根据给定数量和范围生成不重复的题目和答案。

  • write_to_file: 将生成的题目或答案写入文件。

  • read_file: 从文件读取题目或答案。

  • check_answers: 对比题目文件和答案文件,输出正确和错误的编号。

  • write_grades: 将对错题目编号写入文件 Grade.txt。

5. 测试运行

6. 实际运行结果

7. 项目小结

  • 在本次项目中,我们加深了对 Python语言理解和掌握。

  • 通过设计和实现一个具有特定功能的程序——从需求分析到设计,再到代码实现和测试,我们经历了完整的软件开发流程,积累了宝贵的实践经验。

  • 我们学会了共同沟通,共同思考得出解决方法,提高了通过团队解决问题的能力。

posted @ 2024-09-24 22:17  爱喝茶的瓜仔  阅读(26)  评论(0)    收藏  举报