結對程式設計小專案實現 Python+PyQt5+OOP
最開始我們兩個人分別寫個人專案時,分別用的是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: 2 def __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 15 def __str__(self): 16 return self.prev + self.curr + self.next 17 18 19 def add_left_bracket(self): 20 self.prev = '(' + self.prev 21 22 def add_right_bracket(self): 23 self.next += ')' 24 25 def add_square(self): 26 random = Random() 27 length = len(self.next) 28 if length == 0: 29 self.next = '²' 30 return 31 pos = random.randint(0, length - 1) 32 self.next = self.next[0: pos + 1] + '²' + self.next[pos + 1:] 33 34 35 def add_sqrt(self): 36 random = Random() 37 length = len(self.prev) 38 if length == 0: 39 self.prev = '√' 40 return 41 pos = random.randint(0, length - 1) 42 self.prev = self.prev[0: pos + 1] + '√' + self.prev[pos + 1:] 43 44 curr = '' 45 prev = '' 46 next = ''
b. Exp類,代表一個算式表示式。
成員變數只有兩個:
一個存有Item的列表,以及一個儲存對應運算子的列表。
之所以將二者分開儲存,也是為了進一步物件化整個過程,否則各項和運算子容易互相雜糅導致必須進行較為複雜的字串處理操作。
成員函式包括以下幾種:
為表示式新增Item的函式
新增括號的函式
新增乘方的函式
處理三角函式的函式
以字串形式返回表示式的函式
返回運算結果的函式
隨機返回一個運算子的函式(靜態)
處理平方的函式(靜態)
處理平方根的函式(靜態)
具體程式碼如下(已省去較為複雜的函式的實現):
class Exp: def __init__(self): pass def 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): pass def add_power(self): # add ² or √ pass def __str__(self): # return the str of the expression tmp = '' 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 tmp def handle_func(self): pass def get_answer(self): # return the answer pass items = [] op = [] @staticmethod def get_op(): ops = ['+', '-', '*', '/'] random = Random() return ops[random.randint(0, 3)] @staticmethod def handle_square(s): # to compute the square pass @staticmethod def handle_sqrt(s): # to compute the square root, similar to the function above pass
整體的思路是
先判斷難度:
若為高中,則生成帶三角函式的各項加入表示式;否則不加。(用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 item random = 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. 為避免負數開根,在開根號前進行檢查(捕捉異常):
@staticmethod def handle_sqrt(s): # to compute the square root, similar to the function above cnt = s.count('√') while cnt != 0: pos = s.find('√') i = pos + 1 if s[i].isdigit(): j = i while j < len(s) - 1 and (s[j + 1].isdigit() or s[j + 1] == '.'): j += 1 tmp = '' 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 -= 1 else: j = i flag = 1 while flag != 0 and j < len(s) - 1: j += 1 if s[j] == ')': flag -= 1 elif s[j] == '(': flag += 1 tmp = '' 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 -= 1 return 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 = 0 try: res = round(eval(s), 3) except ZeroDivisionError: res = 77777 print('\tException: zero division') finally: return res else: return 77777
對於無法計算的算式,請求返回其答案時會返回固定值77777。
當軟體生成一個題目時,發現其答案為77777,直接跳過該題,並在控制檯輸出錯誤報告。
if res == 77777: self.curr -= 1 print('-----ILLEGAL EXPRESSION, AVOIDED-----') self.set_problem() return
總結:
結對專案歷時數天,因為之前自己就學過Qt,所以UI部分實現難度不大,主要難度還是在於生成算式並計算答案中的演算法中,以及申請阿里雲的API使用權也是一個較為費神的東西。
通過這次專案,也算是進一步體味到了OOP的重要性與優越性,可以讓很複雜的一個程式結構變得非常清晰易懂,一有錯誤也能立刻找出來。