最开始我们两个人分别写个人项目时,分别用的是Java和C++,但是在做这个带UI的升级版后,我坚定地摈弃了之前两个人所写的代码,选择用Python完全重写,原因有以下几点:
1. 之前两个人写的都不够好,在生成算式(尤其是括号匹配等方面)的过程中算法过于繁琐,而且有缺陷(我的只能最多生成一对括号,他的括号分布是固定搭配中的伪随机)。
2. 之前两人采用的算法导致生成算式后并不能很好的进行计算,而结对项目需要生成正确答案。
3. Java下的UI框架Swing过于陈旧,已不是一个相当受开发者欢迎的框架。C++下的Qt由于C++语法过于繁琐,会带来诸多不便。
4. Python有自带的eval函数,该函数能够进行简单的四则运算(但不能计算乘方、三角函数等),带来简便。
于是最后决定使用Python+Qt的搭配,且UI的设计在Qt Designer下进行。
① 核心代码部分
吸取了之前个人项目中因为采用面向过程的思想,结果导致整个算法过于繁琐冗杂,且牵涉到了相当复杂的字符串处理,无论是代码的简洁清晰程度还是程序的运行效率都不够好。
于是这次打算采用思路更加清晰、可塑性更高的面向对象的思想。
关于算式的生成与计算有两个类:
a. Item类,代表算式中的某一项。
主要包括三个成员:前缀,本体与后缀。
前缀包含三角函数、左括号、平方根、负号
后缀包括平方、右括号
本体则是该一项的数值对应的字符串。
包含多个成员函数
构造函数传入一个boolean值,默认为False。当且仅当其为True时会生成带有三角函数的项。
其余的例如插左括号、插右括号等操作就是直接对该个对象的前缀或后缀进行插入,较为简单,不一一介绍。
具体代码如下:
1 class Item: 2def __init__(self, flag=False): # when the flag is true, initialize an trigonometric item 3 random = Random() 4 if flag == True: 5 func = ['sin', 'cos', 'tan'] 6 value = ['0°', '30°', '45°', '60°', '120°', '135°', '150°', '180°', '90°'] 7 choice = func[random.randint(0, 2)] 8 if choice == 'tan': 9 self.curr = choice + value[random.randint(0, 7)]10 else:11 self.curr = choice + value[random.randint(0, 8)]12 else:13 self.curr = str(random.randint(1, 100))14 15def __str__(self):16 return self.prev + self.curr + self.next17 18 19def add_left_bracket(self):20 self.prev = '(' + self.prev21 22def add_right_bracket(self):23 self.next += ')'24 25def add_square(self):26 random = Random()27 length = len(self.next)28 if length == 0:29 self.next = '²'30 return31 pos = random.randint(0, length - 1)32 self.next = self.next[0: pos + 1] + '²' + self.next[pos + 1:]33 34 35def add_sqrt(self):36 random = Random()37 length = len(self.prev)38 if length == 0:39 self.prev = '√'40 return41 pos = random.randint(0, length - 1)42 self.prev = self.prev[0: pos + 1] + '√' + self.prev[pos + 1:]43 44curr = ''45prev = ''46next = ''
b. Exp类,代表一个算式表达式。
成员变量只有两个:
一个存有Item的列表,以及一个存储对应运算符的列表。
之所以将二者分开存储,也是为了进一步对象化整个过程,否则各项和运算符容易互相杂糅导致必须进行较为复杂的字符串处理操作。
成员函数包括以下几种:
为表达式添加Item的函数
添加括号的函数
添加乘方的函数
处理三角函数的函数
以字符串形式返回表达式的函数
返回运算结果的函数
随机返回一个运算符的函数(静态)
处理平方的函数(静态)
处理平方根的函数(静态)
具体代码如下(已省去较为复杂的函数的实现):
class Exp:def __init__(self):passdef append(self, item):self.items.append(item)if len(self.items) != 1: # the first item added self.op.append(Exp.get_op())def add_brackets(self):passdef add_power(self): # add ² or √passdef __str__(self): # return the str of the expressiontmp = ''for i in range(0, len(self.items)):if i == 0:tmp += self.items[i].__str__()else:tmp += self.op[i - 1] + self.items[i].__str__()return tmpdef handle_func(self):passdef get_answer(self): # return the answerpassitems = []op = []@staticmethoddef get_op():ops = ['+', '-', '*', '/']random = Random()return ops[random.randint(0, 3)]@staticmethoddef handle_square(s): # to compute the squarepass@staticmethoddef handle_sqrt(s): # to compute the square root, similar to the function abovepass
整体的思路是
先判断难度:
若为高中,则生成带三角函数的各项加入表达式;否则不加。(用Item的构造函数是否传True来区分)
然后进行添加括号操作,再根据是否是小学题选择添加乘方运算或者不添加乘方运算。
获取运算结果时,先处理算式的三角函数(如果有),再处理算式中的乘方(如果有),最后将处理结果(没有任何三角函数以及乘方运算)的字符串传给eval函数计算结果。
② UI部分
用Qt Designer设计界面。然后设定好各个signal和slot,再编写各个slot的函数即可。
整体只需要两个界面,一个是主界面,一个是答题界面
该部分较为简单,并无特别的技术要求,故不细述。
③ 短信API接口部分
采用阿里云,注册后直接自己编写一个函数调用DEMO即可。
因为接口需要先安装阿里云的库才能用,所以我干脆设定成了每运行一次都安装一次库(用os.system函数)。
④ 中途遇见的问题
这样完全靠系统随机产生的算式,会存在无法运算的情况。例如负数开方,tan90°,或是0作了除数。
从而导致程序直接崩溃,因为无法运算。
解决方案:
1. 为避免生成tan90°,对随机范围进行限定:
def __init__(self, flag=False): # when the flag is true, initialize an trigonometric itemrandom = Random()if flag == True:func = ['sin', 'cos', 'tan']value = ['0°', '30°', '45°', '60°', '120°', '135°', '150°', '180°', '90°']choice = func[random.randint(0, 2)]if choice == 'tan':self.curr = choice + value[random.randint(0, 7)]else:self.curr = choice + value[random.randint(0, 8)]else:self.curr = str(random.randint(1, 100))
2. 为避免负数开根,在开根号前进行检查(捕捉异常):
@staticmethoddef handle_sqrt(s): # to compute the square root, similar to the function abovecnt = s.count('√')while cnt != 0:pos = s.find('√')i = pos + 1if s[i].isdigit():j = iwhile j < len(s) - 1 and (s[j + 1].isdigit() or s[j + 1] == '.'):j += 1tmp = ''try:tmp = str(round(math.pow(float(s[i: j + 1]), 0.5), 3))except Exception:print('\tException: negative square root')return ''s = s[: i - 1] + tmp + s[j + 1:]cnt -= 1else:j = iflag = 1while flag != 0 and j < len(s) - 1:j += 1if s[j] == ')':flag -= 1elif s[j] == '(':flag += 1tmp = ''try:tmp = str(round(math.pow(eval(s[i: j + 1]), 0.5), 3))except Exception:print('\tException: negative value or zero division in square root')return ''s = s[: i - 1] + tmp + s[j + 1:]cnt -= 1return s
3. 为避免除0,在最后一步计算时也捕捉异常。
def get_answer(self): # return the answer self.handle_func()print('\tafter handling the functions: ' + self.__str__())s = Exp.handle_sqrt(Exp.handle_square(self.__str__()))print('\tafter handling the powers: ' + s)if s != '':res = 0try:res = round(eval(s), 3)exceptZeroDivisionError:res = 77777print('\tException: zero division')finally:return reselse:return77777
对于无法计算的算式,请求返回其答案时会返回固定值77777。
当软件生成一个题目时,发现其答案为77777,直接跳过该题,并在控制台输出错误报告。
if res == 77777:self.curr -= 1print('-----ILLEGAL EXPRESSION, AVOIDED-----')self.set_problem()return
总结:
结对项目历时数天,因为之前自己就学过Qt,所以UI部分实现难度不大,主要难度还是在于生成算式并计算答案中的算法中,以及申请阿里云的API使用权也是一个较为费神的东西。
通过这次项目,也算是进一步体味到了OOP的重要性与优越性,可以让很复杂的一个程序结构变得非常清晰易懂,一有错误也能立刻找出来。
如果觉得《结对编程小项目实现 Python+PyQt5+OOP》对你有帮助,请点赞、收藏,并留下你的观点哦!