软件工程第三次作业-结对项目

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13470
这个作业的目标 完成一个自动生成小学四则运算题目的命令行程序
项目 姓名 学号
成员1 陈曦 3223004210
成员2 白子璇 3223004208

Github项目地址:https://github.com/Echooooe/Echooooe/tree/main/结对项目

1.PSP表格(预计时间)

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

2.性能分析

优化前:

生成大量题目时-n 5000 查看程序最耗时的部分

cprofile:

Welcome to the profile statistics browser.
result.prof% sort cumulative
result.prof% stats 20
Thu Oct 16 10:41:01 2025    result.prof

         1717145 function calls (1663174 primitive calls) in 2.168 seconds

   Ordered by: cumulative time
   List reduced from 396 to 20 due to restriction <20>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     13/1    0.000    0.000    2.168    2.168 {built-in method builtins.exec}
        1    0.001    0.001    2.168    2.168 Myapp.py:1(<module>)
        1    0.010    0.010    2.137    2.137 Myapp.py:390(main)
        1    0.069    0.069    2.084    2.084 Myapp.py:231(generate_exercises)
     5765    0.199    0.000    1.289    0.000 Myapp.py:190(gen_expr_with_ops)
    17365    0.102    0.000    0.394    0.000 Myapp.py:143(gen_number)
10746/5765    0.049    0.000    0.287    0.000 Myapp.py:128(canonical)
    38182    0.145    0.000    0.287    0.000 Myapp.py:261(format_fraction_output)
    33182    0.032    0.000    0.271    0.000 Myapp.py:55(to_str)
    12474    0.078    0.000    0.248    0.000 D:\Software\miniconda\Lib\random.py:359(sample)
    31134    0.047    0.000    0.230    0.000 D:\Software\miniconda\Lib\fractions.py:613(forward)
    33537    0.037    0.000    0.229    0.000 D:\Software\miniconda\Lib\random.py:332(randint)
10817/5000    0.062    0.000    0.218    0.000 Myapp.py:95(to_str)
     5765    0.014    0.000    0.212    0.000 Myapp.py:162(validate_tree)
   141894    0.077    0.000    0.199    0.000 {built-in method builtins.isinstance}
28965/5765    0.071    0.000    0.197    0.000 Myapp.py:167(dfs)
    70959    0.128    0.000    0.194    0.000 D:\Software\miniconda\Lib\random.py:242(_randbelow_with_getrandbits)
    33537    0.075    0.000    0.192    0.000 D:\Software\miniconda\Lib\random.py:291(randrange)
16505/9685    0.046    0.000    0.171    0.000 Myapp.py:70(eval)
    17365    0.014    0.000    0.170    0.000 Myapp.py:59(canonical)

pstats性能报告:

微信图片_20251020180514_47_80

函数 ncalls cumtime (s) 占比 说明
gen_expr_with_ops 5,765 1.289 59.4% 核心表达式生成逻辑,递归随机组合节点,验证合法性
gen_number 17,365 0.394 18% 随机生成自然数/分数/带分数
format_fraction_output 38,182 0.287 13% 将 Fraction 转成字符串,包括带分数和真分数
canonical 10,746 / 17,365 0.287 / 0.170 约 12% 去重标准化字符串(对 +/* 做排序归一化)
dfs 28,965 / 5,765 0.197 约 9% validate_tree 中递归判断表达式合法性

当前性能瓶颈主要集中在:

(1) gen_expr_with_ops(最大瓶颈)

  • 占总耗时 >50%,原因:
    1. 每次生成表达式,需要在节点池中随机取两个节点组合;
    2. 每次随机组合后都要检查 减法/除法合法性
    3. 重复尝试的次数很多(MAX_TRIES=20000),容易产生大量无效组合;
    4. dfsvalidate_tree 被反复调用,导致递归计算多次。

(2) gen_number(次大瓶颈)

  • 占用约 18%,原因:
    • 每生成一个节点都会调用多次 random.randint(),分数/带分数的生成逻辑复杂,涉及整数和分母随机选择;
    • 重复计算和条件判断较多。

(3) format_fraction_output / canonical

  • 占用约 12%-13%,原因:
    • Fraction 转字符串、带分数判断和拼接耗时;
    • canonical 需要对 +/* 运算符的子节点排序,频繁调用 to_str()

优化思路:

对gen_expr_with_ops函数:

1.缓存叶子节点值 → 避免重复 eval() 调用。

2.randrange 代替 sample → 减少随机索引生成开销。

3.提前剪枝非法组合 → 不生成不合法节点,减少无用操作。

4.最终一次性验证 → 避免重复 dfs 验证,提高效率。

优化后:

cprofile:

1769853 function calls (1717355 primitive calls) in 1.908 seconds 

   Ordered by: cumulative time
   List reduced from 394 to 20 due to restriction <20>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     13/1    0.000    0.000    1.908    1.908 {built-in method builtins.exec}
        1    0.000    0.000    1.908    1.908 Myapp.py:1(<module>)
        1    0.016    0.016    1.877    1.877 Myapp.py:407(main)
        1    0.065    0.065    1.817    1.817 Myapp.py:248(generate_exercises)
     5783    0.203    0.000    1.125    0.000 Myapp.py:190(gen_expr_with_ops)
    17355    0.090    0.000    0.356    0.000 Myapp.py:143(gen_number)
    67407    0.113    0.000    0.314    0.000 D:\Software\miniconda\Lib\random.py:291(randrange)
10702/5783    0.045    0.000    0.241    0.000 Myapp.py:128(canonical)
    38118    0.109    0.000    0.241    0.000 Myapp.py:278(format_fraction_output)
    33118    0.027    0.000    0.224    0.000 Myapp.py:55(to_str)
    34630    0.048    0.000    0.219    0.000 D:\Software\miniconda\Lib\fractions.py:613(forward)
    33485    0.035    0.000    0.216    0.000 D:\Software\miniconda\Lib\random.py:332(randint)
    79887    0.128    0.000    0.197    0.000 D:\Software\miniconda\Lib\random.py:242(_randbelow_with_getrandbits)
10763/5000    0.045    0.000    0.185    0.000 Myapp.py:95(to_str)
     5783    0.011    0.000    0.185    0.000 Myapp.py:162(validate_tree)
28927/5783    0.066    0.000    0.174    0.000 Myapp.py:167(dfs)
    30146    0.027    0.000    0.167    0.000 D:\Software\miniconda\Lib\fractions.py:953(__lt__)
    17355    0.019    0.000    0.140    0.000 Myapp.py:59(canonical)
    30146    0.053    0.000    0.140    0.000 D:\Software\miniconda\Lib\fractions.py:931(_richcmp)
   133885    0.061    0.000    0.103    0.000 {built-in method builtins.isinstance}

pstats性能报告:

微信图片_20251020180515_48_80

改进前后性能分析结果对比

指标 优化前 优化后 变化
总函数调用数 1,717,145 1,769,853 ↑ +3.1%(轻微增加)
运行总时长 2.168 s 1.908 s ↓ 12% 加速
gen_expr_with_ops() 耗时 1.289 s 1.125 s ↓ 12.7% 加速
gen_number() 耗时 0.394 s 0.356 s ↓ 9.6%
format_fraction_output() 耗时 0.287 s 0.241 s ↓ 16%
主函数 generate_exercises() 总耗时 2.084 s 1.817 s ↓ 12.8%

3.设计实现过程

(1)类和函数设计

①类结构设计

类名 作用 主要方法 说明
Expr 抽象基类 eval()to_str()canonical() 定义表达式统一接口,所有子类继承
Number 数字节点类 eval()to_str() 存储单个数值(自然数或分数)
Binary 二元运算类 eval()to_str()canonical() 存储加、减、乘、除等二叉表达式

②类的继承结构

image

③主要函数设计

函数名 功能 说明
gen_number(rng) 生成随机自然数或真分数 作为表达式叶节点
validate_tree(node) 验证表达式合法性 检查负数、整除、除零
gen_expr_with_ops(k, rng) 生成包含 k 个运算符的表达式 随机生成 + 递归拼接
generate_exercises(n, rng) 批量生成题目与答案 调用上层函数并去重
grade(exfile, ansfile) 比较标准答案与用户答案 输出 Grade.txt 批改结果
parse_and_eval(s) 解析字符串表达式并求值 支持 ÷、带分数格式
format_fraction_output(fr) 将 Fraction 格式化输出 生成整数、分数、带分数形式
parse_mixed_fraction(s) 将“2'3/4”等字符串转 Fraction 解析带分数格式
generate() GUI按钮回调,生成题目文件 调用 generate_exercises()
grade_files() GUI按钮回调,批改文件 调用 grade() 并弹窗提示

(2)关键函数流程图

image

4.代码说明

关键代码与注释说明

(1)表达式类设计(Expr、Number、Binary)
Expr:

class Expr:
    """表达式抽象基类"""
    def eval(self) -> Fraction:
        """计算表达式结果"""
        raise NotImplementedError

    def to_str(self) -> str:
        """将表达式转换为可显示字符串"""
        raise NotImplementedError

    def canonical(self) -> str:
        """生成用于去重的标准化字符串"""
        raise NotImplementedError

思路说明:该类是所有表达式的抽象父类;提供统一接口 eval()(求值)、to_str()(转字符串)、canonical()(标准化形式);子类 Number 与 Binary 继承并实现这些方法。

Number:

class Number(Expr):
    """表示一个数值节点(自然数或分数)"""
    def __init__(self, frac: Fraction):
        self.frac = frac

    def eval(self):
        return self.frac

    def to_str(self):
        """将 Fraction 转换为字符串形式"""
        return format_fraction_output(self.frac)

思路说明:Number 是表达式树的叶子节点;直接存储一个 Fraction(分数或整数);eval() 直接返回该分数的值;to_str() 用于输出可读格式(如 1'1/2)。

Binary:

class Binary(Expr):
    """表示二元运算节点(+ - * /)"""
    def __init__(self, op, left: Expr, right: Expr):
        self.op = op
        self.left = left
        self.right = right

    def eval(self):
        """递归计算表达式结果"""
        a = self.left.eval()
        b = self.right.eval()
        if self.op == '+': return a + b
        if self.op == '-': return a - b
        if self.op == '*': return a * b
        if self.op == '/': return a / b

思路说明:Binary 是二叉树节点,包含运算符和左右子表达式;eval() 递归调用左右子树的 eval();运算均用 Fraction 保持精度;支持加、减、乘、除四种操作。

(2)随机题目生成模块

gen_number:

def gen_number(rng):
    """生成随机数字(自然数或真分数)"""
    if random.random() < 0.5:
        return Number(Fraction(random.randint(0, rng - 1), 1))
    else:
        denom = random.randint(2, rng)
        numer = random.randint(1, denom - 1)
        return Number(Fraction(numer, denom))

思路说明:以随机方式生成一个数字节点;一半概率生成整数,一半生成真分数;用 Python 内置的 Fraction 保证计算精度。

gen_expr_with_ops:

def gen_expr_with_ops(k, rng):
    """生成包含 k 个运算符的随机表达式"""
    leaves = [gen_number(rng) for _ in range(k + 1)]
    nodes = leaves[:]
    while len(nodes) > 1:
        left = random.choice(nodes)
        right = random.choice(nodes)
        if left == right:
            continue
        op = random.choice(['+', '-', '*', '/'])
        node = Binary(op, left, right)
        if not validate_tree(node):  # 确保合法性
            continue
        nodes.remove(left)
        nodes.remove(right)
        nodes.append(node)
    return nodes[0]

思路说明:通过随机组合数字节点构造表达式树;每次合并两个节点成一个新的 Binary;生成过程中实时调用 validate_tree() 检查合法性,防止出现负数、除零、整除等非法情况。

generate_exercises:

def generate_exercises(n, rng):
    """生成 n 道符合约束的题目与答案"""
    exercises, answers, seen = [], [], set()
    while len(exercises) < n:
        k = random.randint(1, 3)
        root = gen_expr_with_ops(k, rng)
        if not root:
            continue
        can = root.canonical()
        if can in seen:  # 去重
            continue
        seen.add(can)
        exercises.append(root.to_str() + ' =')
        answers.append(format_fraction_output(root.eval()))
    return exercises, answers

思路说明:负责批量生成题目;随机选运算符数量(1~3);用 canonical() 确保等价表达式不会重复;输出题目字符串与正确答案列表。

(3)表达式合法性检查

def validate_tree(node: Expr) -> bool:
    """验证表达式是否合法"""
    try:
        def dfs(n):
            if isinstance(n, Number):
                return n.eval()
            a = dfs(n.left)
            b = dfs(n.right)
            if n.op == '-':
                if a < b: raise ValueError('negative')
                return a - b
            if n.op == '/':
                if b == 0: raise ValueError('divzero')
                res = a / b
                if res.denominator == 1:  # 排除整除
                    raise ValueError('div_integer')
                return res
            if n.op == '+': return a + b
            if n.op == '*': return a * b
        dfs(node)
        return True
    except Exception:
        return False

思路说明:递归遍历表达式树;检查是否出现负数、除零或整除结果;如果违反条件则返回 False,确保生成的题目合法、符合小学题型要求。

(4)批改逻辑

def grade(exfile, ansfile):
    """读取题目与答案文件,逐题比对,输出 Grade.txt"""
    with open(exfile, 'r', encoding='utf-8') as f:
        exercises = [line.strip() for line in f if line.strip()]
    with open(ansfile, 'r', encoding='utf-8') as f:
        answers = [line.strip() for line in f if line.strip()]

    correct_idx, wrong_idx = [], []
    for i in range(min(len(exercises), len(answers))):
        expr_text = strip_number_prefix(exercises[i])[:-1].strip()
        got = parse_and_eval(expr_text)       # 正确结果
        ans_val = parse_and_eval(answers[i])  # 用户答案
        if got == ans_val:
            correct_idx.append(i + 1)
        else:
            wrong_idx.append(i + 1)

    with open('Grade.txt', 'w', encoding='utf-8') as f:
        f.write(f"Correct: {len(correct_idx)} ({', '.join(map(str, correct_idx))})\n\n")
        f.write(f"Wrong: {len(wrong_idx)} ({', '.join(map(str, wrong_idx))})\n")
    print('Grade.txt 已生成')

思路说明:逐题对比用户答案与标准答案;使用 parse_and_eval() 计算正确值;比较结果相等即为正确,最终输出统计结果文件 Grade.txt,显示正确和错误题号。

(5)表达式解析函数

def parse_and_eval(s: str) -> Fraction:
    """解析字符串表达式并计算结果"""
    s = s.replace('÷', '/')
    tokens = TOKEN_REGEX.findall(s.replace(' ', ''))
    out_tokens = []
    for tok in tokens:
        if '/' in tok and tok[0].isdigit():
            n, d = tok.split('/')
            out_tokens.append(f'Fraction({int(n)},{int(d)})')
        elif tok.isdigit():
            out_tokens.append(f'Fraction({int(tok)},1)')
        else:
            out_tokens.append(tok)
    expr = ''.join(out_tokens)
    val = eval(expr, {'Fraction': Fraction})
    return val

思路说明:将字符串形式的算式(如 "3 ÷ 2 + 1/4") 转换为 Python 可执行表达式;将分数、带分数转换为 Fraction,最终用 eval() 在安全上下文中求值,返回一个精确分数结果。

(6)分数格式化输出

def format_fraction_output(fr: Fraction) -> str:
    """将 Fraction 格式化为输出形式"""
    if fr.denominator == 1:
        return str(fr.numerator)
    n, d = abs(fr.numerator), fr.denominator
    if n > d:
        whole, rem = n // d, n % d
        sign = '-' if fr < 0 else ''
        return f"{sign}{whole}'{rem}/{d}"
    sign = '-' if fr < 0 else ''
    return f"{sign}{n}/{d}"

思路说明:输出整数、真分数或带分数;
例如:Fraction(5,1) → "5";Fraction(3,2) → "1'1/2";Fraction(-7,3) → "-2'1/3";
保持输出格式统一,方便小学生理解。

5.扩展功能

(1)设计思路
我们在原有命令行系统的基础上开发了图形化界面(Myapp_gui.py)。GUI 基于tkinter实现,用户可通过界面选择模式、输入参数、生成题目或批改结果,无需手动操作文件或命令行。
整个设计采用 逻辑层与界面层分离 思想:

  • 逻辑层(Myapp.py):提供核心算法与函数,如 generate_exercises()、grade()。
  • 界面层(Myapp_gui.py):负责交互操作与结果显示。
  • GUI 直接调用逻辑层函数,实现“图形化封装”的效果。

(2)关键代码片段与说明

① 主窗口与布局创建

root = tk.Tk()
root.title("小学四则运算生成与批改系统")
root.geometry("500x350")

frame_generate = tk.LabelFrame(root, text="生成题目")
frame_generate.pack(fill="x", padx=10, pady=10)

frame_grade = tk.LabelFrame(root, text="批改答案")
frame_grade.pack(fill="x", padx=10, pady=10)

说明:

  • 创建主窗口与两个功能分区(题目生成区与批改区);
  • 采用 LabelFrame 使界面层次清晰;
  • pack() 布局方式保证窗口自适应缩放。

② 生成题目

def generate():
    n = int(entry_num.get())
    rng = int(entry_range.get())
    exercises, answers = Myapp.generate_exercises(n, rng)
    with open("Exercises.txt", "w") as f1, open("Answers.txt", "w") as f2:
        for i in range(n):
            f1.write(f"{i + 1}. {exercises[i]}\n")
            f2.write(f"{i + 1}. {answers[i]}\n")
    messagebox.showinfo("生成成功", "题目与答案已生成!")

说明:

  • 从输入框中读取题目数量与数值范围;
  • 调用逻辑层 generate_exercises() 生成内容,自动保存到 Exercises.txt 与 Answers.txt;
  • 用 messagebox 弹窗提示执行结果。

③ 批改答案

def grade():
    exfile = entry_exfile.get()
    ansfile = entry_ansfile.get()
    Myapp.grade(exfile, ansfile)
    messagebox.showinfo("批改完成", "结果已生成 Grade.txt!")

说明:

  • 通过文件选择框选择题目文件与答案文件;
  • 调用逻辑层 grade() 完成自动批改,结果保存在 Grade.txt,并提示完成。

④ 文件选择

def select_file(entry):
    path = filedialog.askopenfilename(title="选择文件", filetypes=[("Text Files", "*.txt")])
    entry.delete(0, tk.END)
    entry.insert(0, path)

说明:

  • 封装文件选择对话框;
  • 用户无需手动输入文件路径,防止路径错误或找不到文件的问题。

(3)运行界面展示
①生成题目界面
屏幕截图 2025-10-22 082851

屏幕截图 2025-10-22 082857

②文件选择界面

屏幕截图 2025-10-22 085530

③成功批改答案并生成Grade.txt
屏幕截图 2025-10-22 082930

6.测试运行

测试用例

序号 测试函数名 测试目标 测试思路
1 test_gen_number 验证 gen_number 能否正确生成 Number 对象且值为 Fraction 随机生成 50 个 Number,检查类型与 eval() 输出类型
2 test_number_eval 验证 Number.eval() 返回自身存储的 Fraction 构造一个 Number(Fraction),调用 eval() 并与原 Fraction 比较
3 test_binary_eval 验证 Binary.eval() 正确计算加法结果 构造 Binary('+', Number, Number),检查 eval() 与手动计算结果一致
4 test_gen_expr_with_ops_valid 验证 gen_expr_with_ops 能生成合法表达式 生成最多 3 个运算符的表达式,检查对象类型并用 validate_tree 验证合法性
5 test_validate_tree_negative 验证 validate_tree 能检测减法产生负数的非法情况 构造 1 - 2,断言 validate_tree 返回 False
6 test_validate_tree_div_integer 验证 validate_tree 能检测除法结果为整数的非法情况 构造 4 ÷ 2,结果分母为 1,断言 validate_tree 返回 False
7 test_format_fraction_output 验证 format_fraction_output 能正确格式化整数、真分数、带分数和负数 构造多种 Fraction,检查输出字符串是否符合预期
8 test_parse_mixed_fraction 验证 parse_mixed_fraction 能解析带分数、真分数和整数 输入 "3'2/5", "2/3", "5",检查返回的 Fraction 是否正确
9 test_parse_and_eval 验证 parse_and_eval 能正确解析题目字符串并返回 Fraction,支持带分数和 ÷ 符号 构造加法、带分数加法、除法表达式,检查计算结果是否正确
10 test_generate_exercises_count_and_unique 验证 generate_exercises 能生成指定数量的题目和答案,题目去重生效 生成 10 道题目,检查题目和答案长度是否一致,并解析表达式确保值非空

测试结果

微信图片_20251020180516_49_80

7.PSP表格(实际时间)

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

8.commit记录:

微信图片_20251020180517_50_80

9.项目小结

(1)项目概述
本次结对项目的主题是 《小学四则运算题目生成与批改系统》。项目包含命令行与图形化界面(GUI)两种模式,支持题目自动生成、答案批改与结果统计。我们采用 Python + tkinter 技术栈,通过 Fraction 模块保证分数计算精度,并实现了题目合法性校验、去重、异常处理等功能。

(2)项目总结

方面 成功经验 不足与改进方向
功能实现 成功实现题目生成、答案文件生成、自动批改、结果输出等完整流程。 题目难度层次较单一,后续可扩展多级难度或自定义运算符数。
程序设计 采用模块化设计,结构清晰。 代码可进一步封装为类体系,如独立的 ExerciseGenerator 类。
测试与验证 使用 pytest 进行单元测试,验证表达式、分数格式与批改逻辑。 GUI 层暂未进行自动化测试,可后续补充。
团队协作 明确分工,沟通顺畅,能互相检查代码与报告。 时间安排略紧,有些文档部分未能同步修改。

总体而言,项目目标完成度高、系统运行稳定,是一次成功的结对开发实践。

(3)结对开发感受
本次结对开发让我们深刻体会到两人合作编程的乐趣与挑战。当一方负责编码、另一方进行测试时,能够迅速发现逻辑漏洞;讨论设计思路的过程比单独写代码更能激发灵感;共同解决 Bug 的过程也增强了团队协作意识。我们在合作中实现了互补:一方更擅长算法逻辑,另一方更擅长界面与用户体验,使项目最终效果更完整。

(4)建议分享

成员 闪光点 改进建议
成员1(主要负责逻辑与测试) 代码思路清晰,逻辑结构强,注重算法正确性。 可在 GUI 部分投入更多参与,以更好理解整体流程。
成员2(主要负责界面与文档) 界面设计美观、交互友好,报告文档规范。 可进一步加强 Python 数据结构与函数式编程理解。
posted @ 2025-10-22 08:20  bzxxxxxx  阅读(23)  评论(0)    收藏  举报