个人项目互评-中小学数学试卷生成

发布时间 2023-09-20 10:56:39作者: 泠风未垠

个人项目互评-中小学数学试卷生成

队友:软件2104李锦华

队友语言选择:Python

题目要求

个人项目:中小学数学卷子自动生成程序

用户:

小学、初中和高中数学老师。

功能:

1、命令行输入用户名和密码,两者之间用空格隔开(程序预设小学、初中和高中各三个账号,具体见附表),如果用户名和密码都正确,将根据账户类型显示“当前选择为XX出题”,XX为小学、初中和高中三个选项中的一个。否则提示“请输入正确的用户名、密码”,重新输入用户名、密码;

2、登录后,系统提示“准备生成XX数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):”,XX为小学、初中和高中三个选项中的一个,用户输入所需出的卷子的题目数量,系统默认将根据账号类型进行出题。每道题目的操作数在1-5个之间,操作数取值范围为1-100;

3、题目数量的有效输入范围是“10-30”(含10,30,或-1退出登录),程序根据输入的题目数量生成符合小学、初中和高中难度的题目的卷子(具体要求见附表)。同一个老师的卷子中的题目不能与以前的已生成的卷子中的题目重复(以指定文件夹下存在的文件为准,见5);

4、在登录状态下,如果用户需要切换类型选项,命令行输入“切换为XX”,XX为小学、初中和高中三个选项中的一个,输入项不符合要求时,程序控制台提示“请输入小学、初中和高中三个选项中的一个”;输入正确后,显示“”系统提示“准备生成XX数学题目,请输入生成题目数量”,用户输入所需出的卷子的题目数量,系统新设置的类型进行出题;

5、生成的题目将以“年-月-日-时-分-秒.txt”的形式保存,每个账号一个文件夹。每道题目有题号,每题之间空一行;

账户类型 账户 密码 备注
小学 张三1 123
张三2 123
张三3 123
初中 李四1 123
李四2 123
李四3 123
高中 王五1 123
王五2 123
王五3 123

附表-2:小学、初中、高中题目难度要求

小学 初中 高中
难度要求 +,-,*./ 平方,开根号 sin,cos,tan
备注 只能有+,-,*./和() 题目中至少有一个平方或开根号的运算符 题目中至少有一个sin,cos或tan的运算符

运行结果测试

界面

初始界面

用户界面

功能

登录功能

试卷生成

切换用户

文件展示

各个源文件分析

user.py

#!/usr/bin/env python3.10.13
"""Copyright 2023 Li Jinhua

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Author: Li Jinhua
"""
import json
from abc import ABC


def init_users():
    """打开并输入用户信息,生成用户对象。

    Returns:
        list: 包含从文件读取的用户对象的列表。

    Raises:
        FileNotFoundError: 如果未找到用户信息文件。
        json.JSONDecodeError: 如果无法解析用户信息文件。
    """
    with open('pg_files/Users.json', encoding="utf-8") as json_file:
        users_dict = json.load(json_file)
    users_list = []
    for user in users_dict['users']:
        teacher = Teacher(
            user['username'], user['password'], user['level_type'])
        users_list.append(teacher)
    return users_list


def write_users_file(users_list):
    """将用户对象写入用户信息文件。

    Args:
        users_list (list): 包含用户对象的列表。

    Raises:
        FileNotFoundError: 如果无法打开用户信息文件以进行写入。
        json.JSONEncodeError: 如果无法编码用户信息为 JSON 格式。
    """
    users_dict_list = []
    for user in users_list:
        users_dict = {
            'username': user.username,
            'password': user.password,
            'level_type': user.level_type}
        users_dict_list.append(users_dict)

    users_file_dict = {'users': users_dict_list}
    with open('pg_files/Users.json', 'w', encoding="utf-8") as json_file:
        json.dump(users_file_dict, json_file)


# 抽象类 User
class User(ABC):
    """抽象类 User 表示系统用户。

    Attributes:
        username (str): 用户名。
        password (str): 密码。
    """
    username = ""
    password = ""

    def __init__(self, l_username, l_password):
        """初始化 User 类。

        Args:
            l_username (str): 用户名。
            l_password (str): 密码。
        """
        self.username = l_username
        self.password = l_password

    # 抽象方法
    def login_check(self, l_username, l_password):
        """检查用户登录是否正确。

        Args:
            l_username (str): 用户名。
            l_password (str): 密码。

        Returns:
            bool: 如果凭据有效,则返回 True,否则返回 False。
        """
        return l_username == self.username and l_password == self.password


# User 的派生类 Teacher
class Teacher(User):
    """表示教师用户的派生类。

    Attributes:
        level_type (str): 教师的级别类型。
    """
    level_type = ""

    def __init__(self, l_username, l_password, l_type):
        """初始化 Teacher 类。

        Args:
            l_username (str): 用户名。
            l_password (str): 密码。
            l_type (str): 教师的级别类型。
        """
        super().__init__(l_username, l_password)
        self.level_type = l_type

(1) 优点:

① 注释和文档:与其他模块一样,该模块中包含了详细的注释和文档字符串,清晰地解释了类和方法的作用。

② 使用 JSON 存储用户信息:用户信息以 JSON 格式存储在文件中,这是一种通用的方式,可以方便地进行读取和写入。

③ 面向对象编程:使用了面向对象编程的方法,将用户抽象为 User 和教师特例为 Teacher。

④ 用户认证:提供了用户认证的功能,用户可以通过用户名和密码进行登录检查。

⑤ 初始化用户列表:init_users 函数从 JSON 文件中读取用户信息并初始化用户对象列表。

(2) 缺点:

① 异常处理不足:虽然代码中使用了异常处理,但处理的方式相对简单,没有提供足够的反馈信息或针对不同的异常情况采取不同的措施。

pg_question.py

"""Copyright 2023 Li Jinhua

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Author: Li Jinhua
"""
import math
import random
from abc import ABC
from abc import abstractmethod


# 生成题树
def generate_tree(level_type, operations_max, now_operations_num):
    """生成数学题的树形结构。

    Args:
        level_type (str): 数学题的难度级别,可以是"小学"、"初中"或"高中"。
        operations_max (int): 允许的最大操作数数量。
        now_operations_num (int): 当前操作数数量。

    Returns:
        QuestionNode: 生成的数学题树的根节点。
    """
    question_node = None
    if level_type == "小学":
        question_node = ElementaryQN(None)
    elif level_type == "初中":
        question_node = MiddleQN(None)
    elif level_type == "高中":
        question_node = HighQN(None)
    question_node.generate_tree(operations_max, now_operations_num)
    question_node.check_left_right()
    return question_node


class QuestionNode(ABC):
    """表示数学题中的节点的抽象基类。

    Attributes:
        value (str): 节点的值。
        father (QuestionNode): 父节点。
        left (QuestionNode): 左子节点。
        right (QuestionNode): 右子节点。
        weight (int): 操作符权重值。数字为0, "+" 和 "-" 为1, "*" 和 "/" 为2。
    """
    value = ""
    father = None
    left = None
    right = None
    weight = 0  # 操作符权重值

    # 数字:100 "+":1, "-":1, "*":2, "/":2,

    def __init__(self, father):
        """初始化 QuestionNode 对象。

        Args:
            father (QuestionNode): 父节点。
        """
        self.value = ""
        self.father = father
        self.left = None
        self.right = None

    # 判断叶子节点
    def is_leaf(self):
        """判断节点是否为叶子节点。

        Returns:
            bool: 如果节点是叶子节点,返回 True,否则返回 False。
        """
        if self.left is None and self.right is None:
            return True
        return False

    @abstractmethod
    def generate_tree(self, operations_max, now_operations_num):
        """生成数学题的树形结构。

        Args:
            operations_max (int): 允许的最大操作数数量。
            now_operations_num (int): 当前操作数数量。
        """

    @abstractmethod
    def tree_to_question(self):
        """将数学题的树形结构转化为题目字符串。
        """

    @abstractmethod
    def tree_to_answer(self):
        """计算数学题的树形结构的答案。
        """


class ElementaryQN(QuestionNode):
    """表示小学数学题的题目节点。
    """

    def generate_tree(self, operations_max, now_operations_num):
        """生成小学数学题的树形结构。

        1. 根据用户类型创建根节点, 输入操作数最大值
        2. 根节点创建两个子节点, 递归调用 create_children,
         每次修改传入函数的 operations_max
        3. 只剩下一个操作数时, 直接生成随机数值作为操作数
        4. 操作数数量大于1时, 有0.5的概率生成操作符
        5. 操作数数量等于operations_max时一定要生成操作符

        Args:
            operations_max (int): 允许的最大操作数数量。
            now_operations_num (int): 当前操作数数量。

        Returns:
            ElementaryQN: 生成的小学数学题树的根节点。
        """
        if now_operations_num == 1 or random.randint(1, now_operations_num * 2)\
                > now_operations_num != operations_max:
            # 直接生成随机数值作为操作数
            self.value = random.randint(1, 100)
            self.weight = 0
        else:
            # 生成操作符 (+-*/)
            self.generate_operator()
            # 产生左子节点和右子节点
            left_child = ElementaryQN(self)
            right_child = ElementaryQN(self)
            # 每个节点的右子节点不分支, 只能是操作数, 传入 1 作为最大操作数
            self.right = right_child.generate_tree(operations_max, 1)
            # 最大操作数减一
            now_operations_num -= 1
            self.left = left_child.generate_tree(
                operations_max, now_operations_num)
        # 返回当前question_node
        return self

    def generate_operator(self):
        """生成操作符 (+-*/)
        """
        # 生成一个随机整数,范围从0到3,用于选择操作符
        random_operator = random.randint(0, 3)
        # 根据随机整数选择操作符, 并确定操作符权重值
        if random_operator == 0:
            self.value = "+"
            self.weight = 1
        elif random_operator == 1:
            self.value = "-"
            self.weight = 1
        elif random_operator == 2:
            self.value = "*"
            self.weight = 2
        elif random_operator == 3:
            self.value = "/"
            self.weight = 2

    def tree_to_question(self):
        """将小学数学题的树形结构转化为题目字符串。

        Returns:
            str: 转化后的题目字符串。
        """
        question_str = ""
        if self.left.is_leaf() and self.right.is_leaf():
            question_str += str(self.left.value) + " " + str(self.value) + \
                            " " + str(self.right.value)
        else:
            question_str += self.left.tree_to_question() + " " + \
                            str(self.value) + " " + str(self.right.value)
        if self.father is not None:
            if self.weight < self.father.weight and not self.is_leaf():
                question_str = "( " + question_str + " )"
        return question_str

    def tree_to_answer(self):
        """计算小学数学题的树形结构的答案。

        Returns:
            float: 计算出的答案。
        """
        answer = 0
        if self.left.is_leaf() and self.right.is_leaf():
            if self.value == "+":
                answer = float(self.left.value) + float(self.right.value)
            elif self.value == "-":
                answer = float(self.left.value) - float(self.right.value)
            elif self.value == "*":
                answer = float(self.left.value) * float(self.right.value)
            elif self.value == "/":
                answer = float(self.left.value) / float(self.right.value)
        else:
            if self.value == "+":
                answer = self.left.tree_to_answer() + float(self.right.value)
            elif self.value == "-":
                answer = self.left.tree_to_answer() - float(self.right.value)
            elif self.value == "*":
                answer = self.left.tree_to_answer() * float(self.right.value)
            elif self.value == "/":
                answer = self.left.tree_to_answer() / float(self.right.value)
        return answer

    def check_left_right(self):
        """保证小学数学题的两个操作数的题树中左支操作数值小于右支操作数。
        """
        if self.father is None and self.left.is_leaf() and self.right.is_leaf():
            if self.value in ("+", "*"):
                if self.left.value > self.right.value:
                    temp_node = self.left
                    self.left = self.right
                    self.right = temp_node


class MiddleQN(ElementaryQN):
    """表示初中数学题的题目节点。

    Attributes:
        radical_square (int): 根号(1)和平方(2)以及二者皆无(0)的标识。
    """
    # 根号(1)和平方(2)以及二者皆无(0)
    radical_square = 0

    def __init__(self, father):
        """初始化 MiddleQN 对象。

        Args:
            father (ElementaryQN): 父节点。
        """
        super().__init__(father)
        self.radical_square = 0

    def generate_tree(self, operations_max, now_operations_num):
        """生成初中数学题的树形结构。

        只剩下一个操作数时, 直接生成随机数值作为操作数
        操作数数量大于1时, 有0.5的概率生成操作符
        操作数数量等于operations_max时一定要生成操作符

        Args:
            operations_max (int): 允许的最大操作数数量。
            now_operations_num (int): 当前操作数数量。

        Returns:
            MiddleQN: 生成的初中数学题树的根节点。
        """
        if now_operations_num == 1 or random.randint(1, now_operations_num * 2)\
                > now_operations_num != operations_max:
            # 直接生成随机数值作为操作数
            self.value = random.randint(1, 100)
            self.weight = 0
        else:
            # 生成操作符 (+-*/)
            self.generate_operator()
            # 产生左子节点和右子节点
            left_child = MiddleQN(self)
            right_child = MiddleQN(self)
            # 每个节点的右子节点不分支, 只能是操作数, 传入 1 作为最大操作数
            self.right = right_child.generate_tree(operations_max, 1)
            # 给radical_square赋值, 视为添加√或²
            self.right.radical_square = random.randint(1, 2)
            # 最大操作数减一
            now_operations_num -= 1
            self.left = left_child.generate_tree(
                operations_max, now_operations_num)
        # 返回当前question_node
        return self

    def tree_to_question(self):
        """将初中数学题的树形结构转化为题目字符串。

        Returns:
            str: 转化后的题目字符串。
        """
        question_str = ""
        if self is None:
            return question_str
        if self.is_leaf():
            if self.radical_square == 0:
                question_str = str(self.value)
            elif self.radical_square == 1:
                question_str = "√" + str(self.value)
            elif self.radical_square == 2:
                question_str = str(self.value) + "²"
        else:
            question_str += self.left.tree_to_question() + " " + \
                            str(self.value) + " " + \
                            self.right.tree_to_question()
        if self.father is not None:
            if self.weight < self.father.weight and not self.is_leaf():
                question_str = "( " + question_str + " )"
        return question_str

    def tree_to_answer(self):
        """计算初中数学题的树形结构的答案。

        Returns:
            float: 计算出的答案。
        """
        answer = 0
        if self is None:
            return answer
        if self.is_leaf():
            if self.radical_square == 0:
                answer = float(self.value)
            elif self.radical_square == 1:
                answer = math.sqrt(float(self.value))
            elif self.radical_square == 2:
                answer = math.pow((float(self.value)), 2)
        else:
            if self.value == "+":
                answer = self.left.tree_to_answer() + \
                         self.right.tree_to_answer()
            elif self.value == "-":
                answer = self.left.tree_to_answer() - \
                         self.right.tree_to_answer()
            elif self.value == "*":
                answer = self.left.tree_to_answer() * \
                         self.right.tree_to_answer()
            elif self.value == "/":
                answer = self.left.tree_to_answer() / \
                         self.right.tree_to_answer()
        return answer


class HighQN(MiddleQN):
    """表示高中数学题的题目节点。

    Attributes:
        sin_cos_tan (int): sin(1)和cos(2)和tan(3)以及三者皆无(0)的标识。
    """
    # san(1)和cos(2)和tan(3)以及三者皆无(0)
    sin_cos_tan = 0

    def __init__(self, father):
        """初始化 HighQN 对象。

        Args:
            father (MiddleQN): 父节点。
        """
        super().__init__(father)
        self.sin_cos_tan = 0

    def generate_tree(self, operations_max, now_operations_num):
        """生成高中数学题的树形结构。

        只剩下一个操作数时, 直接生成随机数值作为操作数
        操作数数量大于1时, 有0.5的概率生成操作符
        操作数数量等于operations_max时一定要生成操作符

        Args:
            operations_max (int): 允许的最大操作数数量。
            now_operations_num (int): 当前操作数数量。

        Returns:
            HighQN: 生成的高中数学题树的根节点。
        """
        if now_operations_num == 1 or random.randint(1, now_operations_num * 2)\
                > now_operations_num != operations_max:
            # 直接生成随机数值作为操作数
            self.value = random.randint(1, 100)
            self.weight = 0
        else:
            # 生成操作符 (+-*/)
            self.generate_operator()
            # 产生左子节点和右子节点
            left_child = HighQN(self)
            right_child = HighQN(self)
            # 每个节点的右子节点不分支, 只能是操作数, 传入 1 作为最大操作数
            self.right = right_child.generate_tree(operations_max, 1)
            # 给sin_cos_tan赋值, 视为添加sin或cos或tan
            self.right.sin_cos_tan = random.randint(1, 3)
            # 最大操作数减一
            now_operations_num -= 1
            self.left = left_child.generate_tree(
                operations_max, now_operations_num)
        # 返回当前question_node
        return self

    def tree_to_question(self):
        """将高中数学题的树形结构转化为题目字符串。

        Returns:
            str: 转化后的题目字符串。
        """
        question_str = ""
        if self is None:
            return question_str
        if self.is_leaf():
            if self.sin_cos_tan == 0:
                question_str = str(self.value)
            elif self.sin_cos_tan == 1:
                question_str = "sin(" + str(self.value) + ")"
            elif self.sin_cos_tan == 2:
                question_str = "cos(" + str(self.value) + ")"
            elif self.sin_cos_tan == 3:
                question_str = "tan(" + str(self.value) + ")"
        else:
            question_str += self.left.tree_to_question() + " " + \
                            str(self.value) + " " + \
                            self.right.tree_to_question()
        if self.father is not None:
            if self.weight < self.father.weight and not self.is_leaf():
                question_str = "( " + question_str + " )"
        return question_str

    def tree_to_answer(self):
        """计算高中数学题的树形结构的答案。

        Returns:
            float: 计算出的答案。
        """
        answer = 0
        if self is None:
            return answer
        if self.is_leaf():
            if self.sin_cos_tan == 0:
                answer = float(self.value)
            elif self.sin_cos_tan == 1:
                answer = math.sin(float(self.value))
            elif self.sin_cos_tan == 2:
                answer = math.cos((float(self.value)))
            elif self.sin_cos_tan == 3:
                answer = math.tan((float(self.value)))
        else:
            if self.value == "+":
                answer = self.left.tree_to_answer() + \
                         self.right.tree_to_answer()
            elif self.value == "-":
                answer = self.left.tree_to_answer() - \
                         self.right.tree_to_answer()
            elif self.value == "*":
                answer = self.left.tree_to_answer() * \
                         self.right.tree_to_answer()
            elif self.value == "/":
                answer = self.left.tree_to_answer() / \
                         self.right.tree_to_answer()
        return answer

(1) 优点:

① 注释和文档: 代码中包含了详细的注释和文档字符串,清晰地解释了类和方法的作用,以及输入和输出。

② 抽象基类: 使用了抽象基类 (ABC),这有助于定义和约束子类的行为,提高了代码的可维护性和可扩展性。

③ 树形结构生成: 实现了生成树形结构的方法,用于生成数学题的结构,这是一个关键的功能。

④ 面向对象编程: 使用了面向对象编程的方法,将不同难度级别的数学题分为不同的类,增加了代码的可扩展性。

(2) 缺点:

① 缺乏错误处理: 代码中没有足够的错误处理机制,如果在生成树形结构时出现问题,可能会导致异常。应该添加错误处理代码,以增加代码的健壮性。

② 函数过长: 一些函数,如 generate_tree 和 tree_to_question,包含了大量的代码,功能相对较多,可读性较差。可以考虑将其拆分为更小的函数,以提高可读性和维护性。

pg_paper.py

#!/usr/bin/env python3.10.13
"""Copyright 2023 Li Jinhua

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Author: Li Jinhua
"""
import json
import os
import time
import pg_question


def generate_paper(questions_sum, teacher, question_dict):
    """生成试卷。

    生成数学题的树形结构,再从树转化为字符串,进行查重并写入文件

    Args:
        questions_sum (int): 试卷中的题目数量。
        teacher (user.Teacher): 教师对象,包含了题目难度信息。
        question_dict (dict): 已生成的题目字典,用于检查重复。
    """
    question_tree_list = []
    question_list = []
    answer_list = []
    # _ 作为变量名来明确表示它是未使用的
    for _ in range(0, questions_sum):
        while True:
            # 生成题树
            question_tree = pg_question.generate_tree(teacher.level_type, 5, 5)
            # 从题树到题目
            question = question_tree.tree_to_question()
            # 计算答案
            answer = question_tree.tree_to_answer()
            # 查重
            if no_duplication(question, answer, question_dict):
                # 加入question_tree_list
                question_tree_list.append(question_tree)
                # 加入question_list
                question_list.append(question)
                # 加入answer_list
                answer_list.append(answer)
                break
    write_paper_in_file(questions_sum, teacher, question_list, answer_list)


def no_duplication(question, answer, question_dict):
    """检查题目是否重复,并更新题目字典。

    Args:
        question (str): 待检查的题目字符串。
        answer (float): 对应题目的答案。
        question_dict (dict): 题目字典,用于存储已生成的题目及其答案。

    Returns:
        bool: 如果题目不重复,则返回 True,否则返回 False。
    """
    # 检查 question_dict 中是否存在 question 键
    if question in question_dict:
        # 若存在则返回 False
        return False
    # 若不存在, 将 question, answer 加入 question_dict, 返回 True
    question_dict[question] = answer
    return True


def write_paper_in_file(questions_sum, teacher, question_list, answer_list):
    """将试卷相关信息写入文件。

    每个老师都有自己的文件夹,包括题目文件夹和答案文件夹。
    题目文件和答案文件均命名为“年-月-日-时-分-秒.txt”

    Args:
        questions_sum (int): 试卷中的题目数量。
        teacher (user.Teacher): 教师对象,包含了题目难度信息。
        question_list (list): 题目列表。
        answer_list (list): 答案列表。
    """
    # 指定文件夹路径
    folder_path = "pg_files/teachers/" + teacher.username + "/"
    question_path = folder_path + "paper/"
    answer_path = folder_path + "answer/"
    # 如果文件夹不存在,创建它
    if not os.path.exists(question_path):
        os.makedirs(question_path)
    if not os.path.exists(answer_path):
        os.makedirs(answer_path)
    now_time = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime())
    file_name = now_time + ".txt"
    with open(question_path + file_name,
              'w', encoding='utf-8') as question_file:
        with open(answer_path + file_name,
                  'w', encoding='utf-8') as answer_file:
            # 打开文件以供写入
            for i in range(0, questions_sum):
                question_file.write("%d.   %s\n\n" % (i + 1, question_list[i]))
                answer_file.write("%d.   %s\n\n" % (i + 1, answer_list[i]))


def read_question_dict(teacher):
    """从文件中读取题目字典。

    Args:
        teacher (user.Teacher): 教师对象。

    Returns:
        dict: 从文件中读取的题目字典,如果文件不存在,则返回一个空字典。
    """
    # 读取该用户的查重文件
    question_dict_path =\
        "pg_files/teachers/%s/question_dict.json" % teacher.username
    # 检查查重文件是否存在
    if os.path.exists(question_dict_path):
        # 文件存在,打开并读取 JSON 数据
        with open(question_dict_path, 'r', encoding='utf-8') as json_file:
            question_dict = json.load(json_file)
    else:
        question_dict = {}
    return question_dict


def write_question_dict(teacher, question_dict):
    """将题目字典写入文件。

    Args:
        teacher (user.Teacher): 教师对象。
        question_dict (dict): 题目字典,用于存储已生成的题目及其答案。
    """
    # 指定文件夹路径
    folder_path = "pg_files/teachers/%s/" % teacher.username
    # 如果文件夹不存在,创建它
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)
    with open(folder_path + "question_dict.json",
              'w', encoding='utf-8') as json_file:
        # 使用 json.dump() 将数据写入文件
        json.dump(question_dict, json_file)

(1) 优点:

① 注释和文档: 代码中包含了详细的注释和文档字符串,清晰地解释了函数的作用和输入输出。

② 文件操作: 代码中实现了文件的读写操作,包括将试卷写入文件以及读取题目字典,这有助于数据的持久化和恢复。

③ 数据查重: 代码实现了查重功能,确保生成的试卷中不会包含重复的题目,提高了试卷的质量。

(2) 缺点:

① 代码健壮性: 代码没有处理文件操作中的异常情况,如文件夹不存在等情况。应该添加错误处理机制来提高代码的健壮性。

② 函数过于庞大: generate_paper 函数中包含了大量的代码,功能相对较多,可读性较差。可以考虑将其拆分为更小的函数来提高可读性和维护性。

pg_account.py

#!/usr/bin/env python3.10.13
"""Copyright 2023 Li Jinhua

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Author: Li Jinhua
"""
import re


def pg_login(teachers):
    """用户登录功能,验证用户名和密码。

    Args:
        teachers (list): 包含教师对象的列表。

    Returns:
        user.Teacher: 登录成功的教师对象,如果登录失败则返回 None。

    """
    while True:
        # 获取命令行,取出用户名密码
        line_input = input("请输入用户名和密码,两者之间用空格隔开:\n")
        input_list = line_input.split(' ', 1)
        if len(input_list) == 2:
            username = input_list[0]
            password = input_list[1]
            for teacher in teachers:
                if teacher.login_check(username, password):
                    print("当前选择为", teacher.level_type, "出题")
                    return teacher
        print("请输入正确的用户名、密码")


def pg_change_type(teacher, line_input):
    """切换题目类型。

    Args:
        teacher (user.Teacher): 当前登录的教师对象。
        line_input (str): 用户输入的命令行字符串。

    Returns:
        int: 如果成功切换题目类型则返回 1,否则返回 0。
    """

    # 定义正则表达式模式,用于匹配"切换为XX",其中XX为小学、初中或高中
    pattern = r"切换为(小学|初中|高中)"
    match = re.search(pattern, line_input)
    if match:
        selected_option = match.group(1)
        teacher.level_type = selected_option
        return 1

    print("请输入小学、初中和高中三个选项中的一个")

    return 0

(1) 优点:

① 注释和文档: 代码中包含了详细的注释和文档字符串,清晰地说明了函数的作用和输入输出。

(2) 缺点:

① 切换题目类型的实现方式: 切换题目类型的函数 pg_change_type 中使用了正则表达式来解析用户输入,这增加了代码的复杂性。可以考虑使用更简单的方式来实现。

main.py

#!/usr/bin/env python3.10.13
"""Copyright 2023 Li Jinhua

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Author: Li Jinhua
"""
import re
import user
import pg_account
import pg_paper


def main():
    """程序的主函数,用于管理用户登录和生成数学题目。

    主要功能包括初始化系统用户、用户登录、生成试卷、切换题目类型以及保存用户数据。
    """
    # 初始化系统用户 (读文件转化为用户对象)
    teachers_list = user.init_users()

    while True:
        # 登录操作
        now_user = pg_account.pg_login(teachers_list)
        # 登录成功后需要根据命令行内容进行功能选择
        # 将该用户的查重数据从文件读入
        question_dict = pg_paper.read_question_dict(now_user)
        user_choice = 0
        # 主体功能
        while True:
            print("准备生成", now_user.level_type,
                  "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录; "
                  "输入-2将退出系统,保存数据):")
            print("切换类型选项,命令行输入“切换为XX”,"
                  "XX为小学、初中和高中三个选项中的一个")
            # 获取命令行, 取出内容, 转化为命令选项并实现
            line_input = input()
            user_choice = line_to_choice(line_input)
            choice_to_function(user_choice, line_input,
                               now_user, question_dict)
            # 退出操作
            if user_choice == -2:
                break
            if user_choice == -1:
                break
        pg_paper.write_question_dict(now_user, question_dict)
        if user_choice == -2:
            break
    # 退出系统时将所有用户数据写入文档
    user.write_users_file(teachers_list)


def line_to_choice(line_input):
    """将命令行输入的字符串转换为用户选择。

    Args:
        line_input (str): 用户输入的命令行字符串。

    Returns:
        int: 表示用户选择的整数,-2表示退出程序,-1表示退出当前用户,
            1表示生成试卷,2表示切换年级,0表示无效输入。
    """
    user_choice = 0
    # 匹配-2
    user_exit_pattern = r'-2'
    user_exit_match = re.match(user_exit_pattern, line_input)
    # 匹配-1
    user_log_out_pattern = r'-1'
    user_log_out_match = re.match(user_log_out_pattern, line_input)
    # 匹配10到30之间的数字
    questions_sum_pattern = r'^(1[0-9]|2[0-9]|30)$'
    questions_sum_match = re.match(questions_sum_pattern, line_input)
    # 匹配切换年级指令
    switch_pattern = r'^切换为'
    switch_match = re.match(switch_pattern, line_input)
    if user_exit_match:
        # 退出程序
        user_choice = -2
    elif user_log_out_match:
        # 退出当前用户
        user_choice = -1
    elif questions_sum_match:
        # 生成试卷
        user_choice = 1
    elif switch_match:
        # 切换年级
        user_choice = 2
    return user_choice


def choice_to_function(user_choice, line_input, now_user, question_dict):
    """根据用户选择执行相应的功能。

    Args:
        user_choice (int): 表示用户选择的功能
        line_input (str): 用户输入的命令行字符串。
        now_user (user.Teacher): 当前登录的教师对象。
        question_dict (dict): 包含题目和答案的字典。
    """
    if user_choice in (-1, -2):
        return
    if user_choice == 1:
        # 生成试卷
        questions_sum = int(line_input)
        pg_paper.generate_paper(questions_sum, now_user, question_dict)
        print("试卷已生成!")
    elif user_choice == 2:
        switch_pattern = r'^切换为(小学|初中|高中)$'
        switch_match = re.match(switch_pattern, line_input)
        if switch_match:
            now_user.level_type = switch_match.group(1)
            print("用户类型已切换为:", now_user.level_type)
        else:
            print("请输入小学、初中和高中三个选项中的一个")
    else:
        print("无效的命令行输入!")


if __name__ == "__main__":
    main()

(1) 优点:

① 注释清晰: 代码中包含了详细的注释,解释了每个函数和功能的用途,这有助于其他开发人员理解代码。

② 合理的模块划分: 代码使用模块化的方式组织,将不同功能的代码分别放在了user、pg_account和pg_paper模块中,提高了可维护性。

③ 良好的用户界面: 代码提供了简单的命令行用户界面,用户可以通过命令进行操作,易于使用。

(2) 缺点:

① 魔法数值: 在代码中存在一些魔法数值(如-1和-2),这些数值没有进行解释,降低了代码的可读性。最好使用具有描述性的常量来表示这些值。

② 复杂的条件判断: 在line_to_choice函数中使用了多个正则表达式来判断用户输入,这增加了代码的复杂性。可以考虑简化这些判断逻辑。

③ 代码冗余: 在不同地方存在相似的正则表达式匹配逻辑,可以将这些逻辑提取为函数以减少重复代码。

总评

这个Python实现的自动生成数学题目和试卷的系统具有多个模块,包括用户管理、题目生成、试卷生成和主程序等。这个系统在许多方面具有优点,但也存在一些缺点。

首先,该系统的优点包括:

  1. 面向对象设计:系统使用面向对象的设计,将不同的功能模块进行了清晰的划分,例如User、QuestionNode等类,这提高了系统的可扩展性,使其更易于维护和扩展。

  2. 完整流程:系统实现了从题目树生成到试卷生成的完整流程,提供了一个全面的解决方案,使用户能够一站式生成试卷。

  3. 丰富的题目样式:通过利用随机数,系统能够生成各种不同类型的题目,这增加了题目的多样性和难度。

  4. 持久化存储:系统采用文件和JSON格式来存储用户数据和重复题目信息,这有助于数据的长期保存和管理。

  5. 代码注释:为每个模块和函数编写详细的注释,提高了代码的可读性,有助于其他开发人员理解和维护代码。

然而,该系统也存在一些缺点:

  1. 耦合度高:部分模块之间的耦合度还可以进一步优化,例如将用户管理模块独立为一个单独的模块,以提高代码的模块化程度。

  2. 异常处理不足:系统没有完善的异常处理机制,输入错误可能导致程序崩溃。应该添加适当的异常处理代码,以增强系统的健壮性。