李光證 3117004660html
吳子昊 3117004671python
李光證博文地址:https://www.cnblogs.com/Gleez/p/11688368.htmlgit
1、github
1. github項目地址:算法
https://github.com/amekao/SE_work2編程
2. 界面示例:數據結構
生成模式app
批改模式框架
2、PSP表格dom
PSP2.1 |
Personal Software Process Stages |
預估耗時(分鐘) |
實際耗時(分鐘) |
Planning |
計劃 |
|
|
· Estimate |
· 估計這個任務須要多少時間 |
1200 |
1200 |
Development |
開發 |
|
|
· Analysis |
· 需求分析 (包括學習新技術) |
180 |
200 |
· Design Spec |
· 生成設計文檔 |
20 |
30 |
· Design Review |
· 設計複審 (和同事審覈設計文檔) |
30 |
60 |
· Coding Standard |
· 代碼規範 (爲目前的開發制定合適的規範) |
30 |
30 |
· Design |
· 具體設計 |
100 |
100 |
· Coding |
· 具體編碼 |
600 |
720 |
· Code Review |
· 代碼複審 |
200 |
180 |
· Test |
· 測試(自我測試,修改代碼,提交修改) |
100 |
90 |
Reporting |
報告 |
|
|
· Test Report |
· 測試報告 |
30 |
40 |
· Size Measurement |
· 計算工做量 |
10 |
10 |
· Postmortem & Process Improvement Plan |
· 過後總結, 並提出過程改進計劃 |
10 |
10 |
合計 |
|
1310 |
1470 |
3、效能分析
使用line_profiler模塊對時間進行測試:
①從新寫了一個time.py進行測試,對裏面main函數修改,不使用argv來獲取參數,而是直接在程序裏面賦值
②在須要測試的模塊前加上@profiler標誌符
③使用 kernprof -l time.py運行程序,生成time.py.lprof報表
④使用 python -m line_profiler time.py.lprof 查看報表
生成模式:生成10000道範圍在0到10的題目
生成模式核心代碼:
# 生成模式 ques_count = 10000 max_val = 10 random_exercises(ques_count, max_val)
核心模塊random_exercises的用時:
Total time: 5.14441 s File: time.py Function: random_exercises at line 12 Line # Hits Time Per Hit % Time Line Contents ============================================================== 12 @profile 13 def random_exercises(q_c, m_v): 14 """ 15 根據輸入的題目數量和數字範圍,生成題目文件和答案文件 16 :param q_c: 題目數量 17 :param m_v: 數字最大值 18 :return: 在目錄下生成試題文件和答案文件 19 """ 20 1 1028.0 1028.0 0.0 e_open = open("Exercises.txt", "a+") 21 1 1073.0 1073.0 0.0 a_open = open("Answers.txt", "a+") 22 10001 14619.0 1.5 0.1 for i in range(0, q_c): 23 10000 7059101.0 705.9 56.3 post = question.post_order_generator(m_v) 24 10000 217674.0 21.8 1.7 tree = question.post_to_tree(post) 25 10000 178163.0 17.8 1.4 ordinary = question.tree_to_ordinary(tree, []) 26 10000 2002280.0 200.2 16.0 readable_str = question.to_readable(ordinary) 27 10000 109711.0 11.0 0.9 e_open.write(f'{i+1}、{readable_str}\n') 28 29 10000 2428198.0 242.8 19.4 ans = question.count_ans(post) 30 10000 423654.0 42.4 3.4 answer = question.to_with_fraction(ans) # 轉成帶分數 31 10000 103062.0 10.3 0.8 a_open.write(f'{i+1}、{answer}\n') 32 1 576.0 576.0 0.0 e_open.close() 33 1 390.0 390.0 0.0 a_open.close()
批改模式:對剛纔生成的10000條題目的答案進行修改
批改模式核心代碼:
# 批改模式 exercise_path = "Exercises.txt" answer_path = "Answers.txt" make_standard(exercise_path) check_exercise(answer_path)
模塊make_standard的用時:
Total time: 1.72545 s File: time.py Function: make_standard at line 35 Line # Hits Time Per Hit % Time Line Contents ============================================================== 35 @profile 36 def make_standard(e_p): 37 """ 38 根據題目文件生成標準答案StandardAnswers.txt 39 :param e_p: 題目文件的path 40 :return: 生成StandardAnswers.txt 41 """ 42 1 9.0 9.0 0.0 try: 43 1 409.0 409.0 0.0 e_open = open(e_p, "r") 44 except FileNotFoundError: 45 print("找不到該文件") 46 sys.exit() 47 except: 48 print("文件打開失敗,請從新運行程序") 49 sys.exit() 50 1 775.0 775.0 0.0 sa_open = open("StandardAnswers.txt", "a+") 51 1 10663.0 10663.0 0.3 e_lines = e_open.readlines() 52 10001 15022.0 1.5 0.4 for line in e_lines: 53 10000 28078.0 2.8 0.7 ques_num = line.split("、")[0] # 題目序號 54 10000 31358.0 3.1 0.7 ques_str = line.split("、")[1].rstrip('\n') # 去掉前面的序號和後面換行符 55 10000 894383.0 89.4 21.3 ordinary = question.to_unreadable(ques_str) 56 10000 370173.0 37.0 8.8 post = question.ordinary_to_post(ordinary) 57 10000 2341848.0 234.2 55.7 sa = question.count_ans(post) 58 10000 413446.0 41.3 9.8 standard = question.to_with_fraction(sa) 59 10000 98687.0 9.9 2.3 sa_open.write(f'{ques_num}、{standard}\n') 60 61 1 266.0 266.0 0.0 e_open.close() 62 1 673.0 673.0 0.0 sa_open.close()
模塊check_exercises的用時:
Total time: 0.0277891 s File: time.py Function: check_exercise at line 64 Line # Hits Time Per Hit % Time Line Contents ============================================================== 64 @profile 65 def check_exercise(a_p): 66 """ 67 標準答案與用戶答案逐行對比,統計對錯 68 :param a_p: 用戶答案path 69 :return: 生成Grade.txt統計文件 70 """ 71 1 459.0 459.0 0.7 sa_open = open("StandardAnswers.txt", "r") 72 1 264.0 264.0 0.4 a_open = open(a_p, "r") 73 1 5800.0 5800.0 8.6 sa_lines = sa_open.readlines() 74 1 3.0 3.0 0.0 right = [] 75 1 1.0 1.0 0.0 wrong = [] 76 77 10001 10571.0 1.1 15.6 for sa_line in sa_lines: 78 10000 20640.0 2.1 30.5 if sa_line == a_open.readline(): 79 10000 20990.0 2.1 31.0 right.append(sa_line.split("、")[0]) 80 else: 81 wrong.append(sa_line.split("、")[0]) 82 1 94.0 94.0 0.1 sa_open.close() 83 1 32.0 32.0 0.0 a_open.close() 84 85 1 5839.0 5839.0 8.6 grade_open = open("Grade.txt", "a+") 86 1 3019.0 3019.0 4.5 grade_open.write(f'正確{len(right)}題:{right}\n') 87 1 24.0 24.0 0.0 grade_open.write(f'錯誤{len(wrong)}題:{wrong}\n')
具體時間分析看第五部分後面
4、設計實現過程
簡易版類圖
question.py是與數學操做相關的函數,大部分由光證同窗完成
main.py是主函數,負責獲取用戶輸入並調用question裏面函數,由子昊同窗完成
程序有正向輸出和逆向兩部分模塊
正向輸出:逆波蘭表達式列表 → 轉化爲二叉樹 → 轉化爲中序表達式列表 → 轉化爲人看的字符串形式
逆向:人看的字符串形式 → 中序表達式列表 → 逆波蘭表達式列表
逆波蘭表達式的好處是不須要括號,減小程序複雜度,運算順序徹底取決於數字的排列,並且計算能夠方便地經過入棧出棧來實現。
生成題目模式使用的random_exercises模塊就是正向輸出,先生成逆波蘭表達式,使用count_ans函數進行計算,存入Answers.txt中,以後再正向輸出字符串到Exercises.txt
批改題目模式使用make_standard模塊:逆向把Exercises.txt的字符串轉化爲逆波蘭表達式,使用count_ans函數進行計算,保存到StandardAnswers.txt
check_exercises模塊:逐行與Answers.txt對比,正確的就存入right列表,錯誤的就存入wrong列表,最後輸出各列表的元素和元素個數
5、核心代碼講解
1. 隨機生成逆波蘭表達式
1 def post_order_generator(max_value): 2 """ 3 逆波蘭式子生成器,隨機生成一個逆波蘭表達式 4 :param max_value: int類型,表示數值的最大值 5 :return: 存儲逆波蘭表達式的列表 6 """ 7 ch = ('+', '-', '×', '÷') 8 char_num = random.randrange(1, 4) 9 num_num = char_num - 1 10 # suffix_list是逆波蘭表達式列表,先在列表前面插入兩個數字 11 suffix_list = [str(get_random_fraction(max_value)), str(get_random_fraction(max_value))] 12 now_num = 2 13 while char_num or num_num: 14 if now_num >= 2: 15 if char_num and random.randrange(0, 2): 16 suffix_list.append(ch[random.randrange(0, 4)]) 17 now_num = now_num - 1 18 char_num = char_num - 1 19 elif num_num: 20 suffix_list.append(str(get_random_fraction(max_value))) 21 num_num = num_num - 1 22 now_num = now_num + 1 23 else: 24 suffix_list.append(ch[random.randrange(0, 4)]) 25 char_num = char_num - 1 26 now_num = now_num - 1 27 else: 28 suffix_list.append(str(get_random_fraction(max_value))) 29 now_num = now_num + 1 30 num_num = num_num - 1 31 st = ".".join(suffix_list) 32 if st in problem_set or count_ans(suffix_list) < 0: 33 suffix_list = post_order_generator(max_value) 34 return suffix_list 35 else: 36 problem_set.add(st) 37 return suffix_list
逆波蘭表達式的限制是第一二個元素必須是數字,最後一個元素必須是運算符,另外數字個數是運算符個數+1
本模塊是先隨機出式子有n個運算符,則數字有n+1個,除去前兩個數先放入列表,和列表最後的運算符,則剩下的符號數和數字數都是n-1,以後在剩下位置隨機生成
另外將生成的逆波蘭表達式轉化爲字符串存入一個set集合中,因爲集合的惟一性,若是新生成的表達式已經在集合中,則這條式子要從新生成
遺憾的是3+4和4+3的判重咱們仍是沒有成功實現
2. 逆波蘭轉二叉樹
1 def post_to_tree(post_list): 2 """ 3 把逆波蘭式轉換成一棵模擬二叉樹 4 :param post_list: 逆波蘭式列表 5 :return: 一個列表,表示一棵樹 6 """ 7 ch = ('+', '-', '×', '÷') 8 temp = [] 9 for v in post_list: 10 if v in ch: 11 t1, t2 = temp.pop(), temp.pop() 12 temp.append([t2, t1, v]) 13 else: 14 temp.append(v) 15 return temp[0]
逆波蘭表達式的兩個數字存入葉子結點,運算符存入根結點,成爲一棵二叉樹
3. 二叉樹轉中序表達式
1 def tree_to_ordinary(tree_list, medium_list): 2 """ 3 把二叉樹轉換成普通表達式 4 :param tree_list: 二叉樹的列表 5 :param medium_list:中序表達式的列表,主要爲了遞歸調用,剛開始可傳一個空列表 6 :return:一個普通表達式的列表 7 """ 8 ch_val = {'+': 1, '-': 1, '×': 2, '÷': 2} # 符號優先級 9 if type(tree_list[0]) == list: 10 if ch_val[tree_list[2]] > ch_val[tree_list[0][2]]: 11 medium_list.append('(') 12 medium_list = tree_to_ordinary(tree_list[0], medium_list) 13 medium_list.append(')') 14 else: 15 medium_list = tree_to_ordinary(tree_list[0], medium_list) 16 else: 17 medium_list.append(tree_list[0]) 18 medium_list.append(tree_list[2]) 19 if type(tree_list[1]) == list: 20 medium_list.append('(') 21 medium_list = tree_to_ordinary(tree_list[1], medium_list) 22 medium_list.append(')') 23 else: 24 medium_list.append(tree_list[1]) 25 return medium_list
對於這一棵二叉樹,後序遍歷就是逆波蘭表達式(後綴表達式),中序遍歷就是中序表達式(平時用的樣式)
難點在於× ÷號的優先級比+ -號要高,遇到這種狀況須要加括號
4. 計算逆波蘭表達式
1 def count_ans(post_list): 2 """ 3 計算逆波蘭式的結果和判斷這條式子是否合法, 4 若是中途出現負數或者除零都會返回-1,不然返回最終結果 5 :param post_list: 逆波蘭式的列表 6 :return: Fraction類型的結果 7 """ 8 ch = ('+', '-', '×', '÷') 9 stack = [] 10 for v in post_list: 11 if v in ch: 12 t1, t2 = stack.pop(), stack.pop() 13 try: 14 if v == '+': 15 t1 = t1 + t2 16 elif v == '-': 17 t1 = t2 - t1 18 elif v == '×': 19 t1 = t1 * t2 20 else: 21 t1 = t2 / t1 22 if t1 < 0: 23 return -1 24 stack.append(t1) 25 except ZeroDivisionError: 26 return -1 27 else: 28 stack.append(Fraction(v)) 29 return stack[0]
使用逆波蘭表達式的計算就是由於式子中沒有括號因此容易寫程序,
將逆波蘭表達式列表當作棧來使用,
新建一個ch元祖存+ - x ÷四個運算符,而後對逆波蘭式列表中元素逐個遍歷,若是元素是運算符,則把運算符前兩個元素出棧進行計算再入棧,直到棧中沒有運算符爲止
回看效能分析:
生成模式:
1. 能夠看到隨機生成逆波蘭表達式post_order_generator是最耗費時間的,佔了56%的時間,猜想是由於random函數比較耗時,而一條式子平均須要5次random
2. 第二耗時間的是計算逆波蘭count_ans,由於每個列表元素都要遍歷一下看在不在ch元祖裏面,並且要作乘除法
3. 第三耗時間的讓人驚奇,竟然是轉成可閱讀to_readable,多是將假分數轉化爲帶分數須要作除法
批改模式:
1. make_standard模塊中第一耗時是計算函數count_ans,理由同上
2. make_standard模塊中第二耗時是轉成不可閱讀to_unreadable,做爲to_readable的反函數,理由也是由於須要作乘法通分把整數部分加回去分子中
3. check_exercises模塊中最耗時是逐行對比部分,該部分是這個模塊最核心的代碼,並且由於須要字符串匹配,看兩邊字符串是否同樣
6、測試用例
因爲本程序的特殊性,運行一次就能夠生成n條測試,極大方便咱們判斷對錯,如下給出生成15條算式時的例子
Myapp.exe -n 15 -r 10
生成的Exercises.txt
一、4'1/2 - 2/5 二、3/4 × 1/2 三、2'2/3 ÷ 1/2 + 1 四、2 × 4'1/2 ÷ 1'2/5 五、2/3 - ( 1/7 - ( 0 × 1'1/7 ) ) 六、5/9 ÷ 5 七、0 × 7/9 八、2 + 1'3/4 九、1'1/5 ÷ 1 十、7/9 ÷ 1 十一、1 + 1'1/2 + 4 十二、0 × 0 × 7/8 1三、( 5/7 + 1'1/5 ) ÷ 1/4 1四、4 × 3 1五、1'1/7 × ( 0 + 1/3 )
生成的答案Answers.txt
一、4'1/10 二、3/8 三、6'1/3 四、6'3/7 五、11/21 六、1/9 7、0 八、3'3/4 九、1'1/5 十、7/9 十一、6'1/2 12、0 1三、7'23/35 1四、12 1五、8/21
對其中的2 3 5 7 11 13題故意改爲錯誤答案,存爲MyAnswers.txt
一、4'1/10 二、3/9 三、6'1/4 四、6'3/7 5、 六、1/9 七、5 八、3'3/4 九、1'1/5 十、7/9 十一、6 12、0 1三、7'23/36 1四、12 1五、8/21
使用以下代碼執行批改模式
Myapp.exe -e Exercises.txt -a MyAnswers.txt
生成的標準答案StandardAnswers.txt和原來的Answers.txt徹底一致
一、4'1/10 二、3/8 三、6'1/3 四、6'3/7 五、11/21 六、1/9 7、0 八、3'3/4 九、1'1/5 十、7/9 十一、6'1/2 12、0 1三、7'23/35 1四、12 1五、8/21
批改結果Grade.txt也判斷正確
正確9題:['1', '4', '6', '8', '9', '10', '12', '14', '15'] 錯誤6題:['2', '3', '5', '7', '11', '13']
7、小結
1. 子昊對光證的
我的認爲本次結對編程項目很是成功,你們都發揮了各自的所長,李光證同窗是ACM隊的,對算法比較熟悉,我由於在工做室作過項目,對使用python比較有經驗
一開始咱們討論了兩種方案,方案一是由內往外生成,即5 x(6 +(7 + 5)),從7+5開始生成,7+5和5+7等價,再到6+12和12+6等價,不過這樣會引入不少無用的括號並且括號括的位置也千奇百怪;
所以後來選擇了使用逆波蘭表達式的方案,計算能夠直接對列表出棧入棧,也省卻了括號的麻煩,使用樹便可把逆波蘭表達式轉爲日常的中序表達式。
光證對算法方面比較熟悉,因此負責逆波蘭表達式的生成,計算,轉化,其餘零散的部分則由我完成。
此次是光證先寫出他的部分,(他花了大概5小時)再由我來組裝,我在剛接手光證代碼時,被一堆奇奇怪怪的數據結構方面的用法搞懵了,徹底看不懂·,並且代碼很亂,不符合PEP8規範,在Pycharm中全是波浪線看着很難受,因此花了一點時間教會了光證用pycharm的自動pep8,並使用‘’‘’‘’來註釋函數功能,這樣在懸停時就能夠看到這個函數到底是作什麼的,不過光證的150多行代碼仍是花了一個多小時才徹底看懂。
後來就是根據本身的理解把光證的函數封裝好,用時大約6小時,程序就寫完了,後面再用了約2個小時對框架進行優化,將模塊分得更細,更容易維護。因此整個開發流程仍是比較舒服的,比我的項目快了好多,表揚光證。
因此此次的成功合做,一是分工明確,光證負責數學函數,我負責封裝; 二是接口定義好,相對比較獨立; 三是我和光證python水平差很少,並且在同一個宿舍方便交流; 四是你們都很好完成了本身的任務,沒有互相拖沓。
但願之後還有更多相似這樣完美的合做。
2. 光證對子昊的
通過多天的努力,本次的結對編程項目完美結束,在此次的結對項目中,收益匪淺。
子昊在工做室裏面作過項目,有過合做的經驗,而且對python的使用有經驗,而我是學校ACM集訓隊的一員,對於算法的設計、實現比較熟悉。因此我主要負責編寫關於表達式的生成,轉換還有計算和分數之間的轉換等功能函數,而子昊負責把我寫的函數整合在一塊兒,對於輸入輸出的編寫還有程序的性能分析。
由於我是參加ACM競賽的,平時敲的變量名能有多短就寫多短,因此此次寫代碼的時候也奇奇怪怪的,因此第一次給子昊的時候他很難理解,而後子昊就來給我講了python代碼的規範,還有教會了我使用pycharm的一些竅門,我改完以後就行了不少。這可能就像蕭伯納所說的那樣「你有一個蘋果,我有一個蘋果,咱們交換一下,一人仍是隻有一個蘋果;你有一個思想,我有一個思想,咱們交換一下,一人就有兩個思想」。
而本次也有許多的不足之處,好比表達式重複的問題我只排除了後綴表達式相同的狀況而已,還有的就是最後函數沒有封裝進類裏面。