20162120 《Python程序设计》实验二 实验报告

20162120 2019-2020-2 《Python程序设计》实验二报告

课程:《Python程序设计》
班级: 1621
姓名: 郑智辉
学号:20162120
实验教师:王志强
实验日期:2020年4月11日
必修/选修: 公选课

1.实验内容

(1)设计并完成一个完整的应用程序,完成加减乘除模等运算,功能多多益善。
(2)考核基本语法、判定语句、循环语句、逻辑运算等知识点。

2. 实验过程及结果

(1)设计思路

该实验目标是制作一个计算器,能够完成加减乘除、求余等运算,我选择参考手机系统自带的计算器进行实现。简单的运算能够使用运算符直接计算,开平方根等复杂运算通过Python的标准库math实现。另一方面,使用pyqt5实现计算器的UI界面,便于用户使用和规范用户输入。输入完成后,对字符串进行识别并完成相应的计算过程。在设计阶段设想能否在用户输入的同时处理算式,简化代码,但由于要给用户提供退格功能,无法实现这个设想。于是采用了大三在编译原理课程上学到的知识:使用符号优先表和双栈算法来解决此问题。

(2)算法逻辑

双栈算法,即“边存边看,边走边算”,设置符号栈和数字栈,符号栈中存储运算符,数字栈中存储数字。同时设置符号优先表,为所有可能出现的符号设置优先级。程序从左到右扫描算式,遇到数字则将其压进数字栈;遇到符号则比较该符号与符号栈栈顶元素的优先级,若当前符号的优先级高,则将其压栈,否则取数字栈栈顶元素,与符号一起完成计算,并将结果压入数字栈。迭代整个过程,直到算式被识别完毕。

(3)主要代码介绍

部分代码有参考。
报告中只贴出了部分代码,全部代码已上传码云:运算部分用户界面部分(使用qt designer生成.ui文件,通过pyqt5库将其转换为.py文件)

  • 计算函数:
    完成数字、运算符弹栈后的计算操作,阶乘、对数、开平方根等复杂运算通过math库完成
import math
def calculate(operator, n1, n2=0):
    """
    :param n1: float
    :param n2: float
    :param operator: + - * / ^ % ! lg ln √
    :return: float
    """
    if operator == "+":
        return n1 + n2
    if operator == "-":
        return n1 - n2
    if operator == "*":
        return n1 * n2
    if operator == "/":
        return n1 / n2
    if operator == '^':
        return n1 ** n2
    if operator == '%':
        return n1 % n2
    if operator == '!':
        return math.factorial(n1)
    if operator == 'lg':
        return math.log10(n1)
    if operator == 'ln':
        return math.log(n1)
    if operator == '√':
        return math.sqrt(n1)
    return 0
  • 符号优先级判断函数:
    根据符号优先表,对比判断当前符号与符号栈栈顶符号的优先级,对数运算与平方根运算是一元运算,因此比乘、除等二元运算优先级高。
def decision(last_op, new_op):
    """
    :param last_op: 运算符栈的最后一个运算符
    :param new_op: 从算式列表取出的当前运算符
    :return: 1 代表弹栈运算,0 代表弹运算符栈最后一个元素, -1 表示入栈
    """
    # 定义5种运算符级别
    grade1 = ['+', '-']
    grade2 = ['*', '/', '%', '!', '^']
    grade3 = ['lg', 'ln', '√']
    grade4 = ['(']
    grade5 = [')']

    if last_op in grade1:
        if new_op in grade2 or new_op in grade3 or new_op in grade4:
            # 说明连续两个运算优先级不一样,需要入栈
            return -1
        else:
            return 1

    elif last_op in grade2:
        if new_op in grade3 or new_op in grade4:
            return -1
        else:
            return 1

    elif last_op in grade3:
        if new_op in grade4:
            return -1
        else:
            return 1

    elif last_op in grade4:
        if new_op in grade5:
            return 0  # ( 遇上 ) 需要弹出 (,丢掉 )
        else:
            return -1  # 只要栈顶元素为(,当前元素不是)都应入栈。
    else:
        return -1
  • 过程函数:
    实现了双栈算法的函数,主要思路如上文所述。与二元运算不同的是,对数运算、平方根运算等一元运算只需要数字栈栈顶的一个元素即可。另外由于乘方运算的特殊性,函数中代表乘方运算的“!”符号一旦出现,就与数字栈栈顶的元素一起完成运算,并将结果压入数字栈,即乘方运算是不进入符号栈的。
def final_calc(formula_list):
    num_stack = []  # 数字栈
    op_stack = []  # 运算符栈
    for e in formula_list:
        operator = is_operator(e)
        if not operator:
            # 压入数字栈
            # 字符串转换为符点数
            num_stack.append(float(e))
        else:
            # 如果是运算符
            while True:
                # 如果该运算符是“!”,则不进入符号栈,马上算出数字栈栈顶元素的乘方,并将结果压入数字栈
                if e == '!':
                    num1 = num_stack.pop()
                    num_stack.append(calculate(e, num1))
                    break
                # 如果运算符栈等于0无条件入栈
                if len(op_stack) == 0:
                    op_stack.append(e)
                    break

                # decision 函数做决策
                tag = decision(op_stack[-1], e)
                if tag == -1:
                    # 如果是-1压入运算符栈进入下一次循环
                    op_stack.append(e)
                    break
                elif tag == 0:
                    # 如果是0弹出运算符栈内最后一个(, 丢掉当前),进入下一次循环
                    op_stack.pop()
                    break
                elif tag == 1:
                    # 如果是1弹出运算符栈内最后两个元素,弹出数字栈最后两位元素。
                    op = op_stack.pop()
                    if op == 'lg' or op == 'ln' or op == '√':
                        num1 = num_stack.pop()
                        num_stack.append(calculate(op, num1))
                    else:
                        num2 = num_stack.pop()
                        num1 = num_stack.pop()
                        # 执行计算
                        # 计算之后压入数字栈
                        num_stack.append(calculate(op, num1, num2))
    # 处理大循环结束后 数字栈和运算符栈中可能还有元素 的情况
    while len(op_stack) != 0:
        op = op_stack.pop()
        if op == 'lg' or op == 'ln' or op == '√':
            num1 = num_stack.pop()
            num_stack.append(calculate(op, num1))
        else:
            num2 = num_stack.pop()
            num1 = num_stack.pop()
            # 执行计算
            # 计算之后压入数字栈
            num_stack.append(calculate(op, num1, num2))
    return num_stack, op_stack
  • 用户界面
    使用pyqt5库和qt designer,仿照手机计算器完成的用户界面。
class MyWindows(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        # 忽略pycharm检测失误导致的警告信息↓
        # noinspection PyArgumentList
        QtWidgets.QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.e = str(math.e)
        self.pi = str(math.pi)
        self.zero_button.clicked.connect(lambda: self.get_formula('0'))
        self.one_button.clicked.connect(lambda: self.get_formula('1'))
        self.two_button.clicked.connect(lambda: self.get_formula('2'))
        # 共29个按键,隐藏部分相似代码,全部代码见上文中的码云链接
        
        self.clear.clicked.connect(self.clean_screen)
        self.back_space.clicked.connect(self.delete_char)
        self.equa_button.clicked.connect(self.get_result)

        self.formula = ''

    def set_cursor(self):
        # 设置输出框游标
        self.showout.ensureCursorVisible()
        cursor = self.showout.textCursor()
        pos = len(self.showout.toPlainText())
        cursor.setPosition(pos)
        self.showout.setTextCursor(cursor)

    def get_formula(self, param):
        # 获取输入
        if param == 'lg' or param == 'ln':
            temp_param = param + '('
        elif param == 're':
            temp_param = '^(-1)'
        else:
            temp_param = param
        self.formula += temp_param
        self.showout.insertPlainText(temp_param)
        self.set_cursor()
        # print(self.formula)

    def clean_screen(self):
        # 清空输出框
        self.formula = ''
        self.showout.clear()
        self.set_cursor()

    def delete_char(self):
        # 退格
        self.showout.ensureCursorVisible()
        cursor = self.showout.textCursor()
        cursor.deletePreviousChar()
        self.formula = self.formula[:-1]

    def get_result(self):
        # 运算
        formula_list = formula_format(self.formula)
        # print(formula_list)
        result, _ = final_calc(formula_list)
        # print(result[0])
        show_result = '=' + str(result[0])
        self.showout.append(show_result)
        self.formula = ''
        self.showout.insertPlainText('\n')
        self.set_cursor()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    main_window = MyWindows()
    main_window.setFixedSize(main_window.width(), main_window.height())

    main_window.show()
    sys.exit(app.exec_())

在qt designer中设计的.ui文件如下图所示:

(4)程序结果

如下图所示,图片左边为我的程序结果,右边为手机计算器的运算结果。从两者对比来看,本程序的运算能力有所保障。

3. 实验过程中遇到的问题和解决过程

  • 问题1:输出框不能自动滚动到最底部
  • 问题1解决方案:在博客园找到了解决方案,虽然不完全贴合我的情况,但稍加改动后就实现了想要的效果。
  • 问题2:不知道如何实现退格功能
  • 问题2解决方案:在一位大佬的个人博客里找到了实现的方法,在文章大概中间的位置介绍了如何实现QTextEdit的两种删除操作。
  • 问题3:在pyqt5启动的用户界面下,程序运算出错时进程会直接崩溃退出,且不会在cmd中trace back,给debug带来了很多麻烦。
  • 问题3解决方案:不要直接运行,而是使用调试模式,这样出错能够看到trace back。这也提醒了我在调试程序阶段不要直接运行,使用调试模式更稳妥。

其他(感悟、思考等)

  • 趁着这次实验,复习了一年前学习的编译原理相关知识,再次感受到了有效、高效的算法是多么的强大。
  • 个人感觉这次实验难度不大,但能够这么快完成,得多谢前人留下的经验教训和简洁易懂的代码。写程序不必重复造轮子,但必须会造轮子。在参考别人的成果时,只是复制粘贴对自己的无益的,必须在充分理解算法的含义,才能促进自己成长。

参考资料

posted @ 2020-04-11 22:43  稚晖  阅读(298)  评论(0编辑  收藏