2017软件工程第2次个人作业
$Github$链接: https://github.com/hz5612777/
$PSP2.1$表格:
|
$PSP2.1$ |
$Personal\ Software\ Process\ Stages$ |
预估耗时(分钟) |
实际耗时(分钟) |
|
$Planning$ |
计划 |
$60$ |
$30$ |
|
$· Estimate$ |
· 估计这个任务需要多少时间 |
$4560$ |
$4145$ |
|
$Development$ |
开发 |
$4170$ |
$4040$ |
|
$· Analysis$ |
· 需求分析 (包括学习新技术) |
$30$ |
$60$ |
|
$· Design\ Spec$ |
· 生成设计文档 |
$240$ |
暂无 |
|
$· Design\ Review$ |
· 设计复审 (和同事审核设计文档) |
$420$ |
暂无 |
|
$· Coding\ Standard$ |
· 代码规范 (为目前的开发制定合适的规范) |
$120$ |
$120$ |
|
$· Design$ |
· 具体设计 |
$480$ |
$420$ |
|
$· Coding$ |
· 具体编码 |
$1500$ |
$2000$ |
|
$· Code\ Review$ |
· 代码复审 |
$300$ |
$240$ |
|
$· Test$ |
· 测试(自我测试,修改代码,提交修改) |
$1080$ |
$1200$ |
|
$Reporting$ |
报告 |
$330$ |
$75$ |
|
$· Test Report$ |
· 测试报告 |
$240$ |
暂无 |
|
$· Size Measurement$ |
· 计算工作量 |
$30$ |
$30$ |
|
$· Postmortem\ Process\ Improvement\ Plan$ |
· 事后总结, 并提出过程改进计划 |
$60$ |
$45$ |
|
合计 |
|
$4560$ |
$4145$ |
1 解题思路
在大体了解本题后,我着重看了"题目描述"里面加粗字体提的要求:操作数、运算符的种类和顺序以及运算符个数必须随机生成,所以首先的思考方向是怎么完成这些"随机"的要求。
继续思考下去,这些"随机"最后都是为了产生一个四则运算的题目,那么,如何确定出这个四则运算的题目?我想可以通过运算符的个数来确定出题目的长度,如果一个题目的长度确定,那么只需要再生成若干随机的操作数就可以确定出题目。我脑海里大概是这么个动图:
- 随机生成运算符的个数n,n在1到10以内
- 按照n的值随机生成运算符的种类和顺序
- 随机生成n+1个操作数,想办法让系统能将真分数以一个整体的形式处理
- 将n+1个操作数分别至于每个运算符的两侧
- 想办法利用字符串处理等方法打印出题目
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/22更新
在我看来,首先需要实现的是“随机”这个要求。于是我查了python如何产生随机数,从而开始了解python的random这个模块。我发现,利用这个模块可以产生指定范围内的随机整数,可以在指定的几种字符串内随机选择某一种字符串,我利用这个模块提供的功能,产生了0~99的随机整数,并实现四种运算符的随机选取。
第二个需要解决的问题是真分数的随机选取,如果将真分数的分子和分母拆为两个数来处理,在进行四则运算的时候,可能会因为不满足运算顺序而导致结果错误,所以可以定义一个描述真分数的类,设置分子、分母两个属性,并将分子和分母组合为一个整体进行处理。有了这个想法,我查了python处理真分数的类和模块,找到了一个可以处理真分数的Fraction()类,它输入分子、分母两个参数,返回一个真分数。
基于这些方法,我可以设计出产生四则运算题目并计算结果的程序。
在实现判断答题者做题正误并统计正确率这个功能时,我想可以用numpy中的二维数组记录答题者做题过程中遇到的题目、自己给出的答案和是否正确等信息,最终完成统计工作。
在实现答题时间控制的功能时,可以将程序设置为双线程,定义全局变量,父线程控制答题的时间,子线程运行答题程序,当达到时间时,改变全局变量的值,子线程检测全局变量的值满足某一条件,停止子线程,即结束答题。
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/26更新
通过判断用户输入,并在12月22日用二维数组完成答题者做题正误的记录后,可以进一步的统计答题者的正确率,并将整个测试过程中的试题、正确答案、答题者答案等信息输出到txt文本。再利用捕捉异常的方式,来解决除数为0时的错误试题和题目发生重复时的错误情况。根据参数控制要求,设计test和practice两种使用模式,test模式下设置测试时间答题,practice模式下设置题目数量答题。
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/28更新
在计算答题者的得分时,我先确定出每个题目的难度系数,再确定出每题的分值,最终得到答题者总分。
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/26更新
2 设计实现过程
根据解题思路,设计该程序需要的函数、类如下表所示:
|
函数名 |
功能 |
|
produce_operators |
生成指定数量的随机运算符 |
|
produce_numbers |
生成指定数量的随机整数和随机真分数 |
|
produce_problem |
生成题目 |
|
display_problem |
打印题目 |
|
calculate_result |
计算题目正确结果 |
|
record_message |
记录答题信息 |
|
judge |
判断答题者答案是否正确 |
|
isrepetition |
判断题目是否有可能重复(设置异常) |
|
test |
对答题者进行测试 |
|
calculate_accuracy |
计算测试正确率 |
|
output_testdata |
打印测试试题等信息 |
|
test_time |
设计测试时间,并驱动线程完成答题时间控制 |
|
类名 |
功能 |
|
timer0 |
继承自threading.Thread类,启动该线程会不断的产生四则运算试题 |
它们之间的关系,我认为可以分为三部份:
- 实现四则运算题目的产生、支持真分数的运算、统计答题者正确率和输出答题信息等功能
- 实现做题时的计时功能
- 通过异常检测排除除数为0的错误题目或重复的题目
(1)
图 1
第一部分在函数test()中执行,其执行了produce_operators、produce_numbers、produce_problem、calculate_result、display_problem和record_message共六个函数。通过produce_operators、produce_numbers、produce_problem产生四则运算题目,calculate_result函数得到产生的四则运算题目的正确答案,display_problem函数将题目打印至控制台,控制台读取用户输入后再通过record_message函数完成数据的录入。
答题完成后,根据录入的数据可以输出答题信息,同时利用calculate_accuracy函数完成用户答题正确率的计算。
(2)
图 2
第二部分由test_time函数驱动主线程和timer类子线程分别开始执行任务,在主线程内进行计时,到达指定时间后,输出控制信号结束子进程的运行。
(3)

图 3
第三部分需要说明的是,当运行try test()命令时,之所以会捕捉NameError异常,是因为在函数isrepetition中,判断出题目为重复时,让程序抛出了NameError。
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/28更新
在设计题目的难度系数时,可分为两部分来进一步确定,第一部分难度是运算符的多少,运算符越多,题目的难度肯定更大。当然,加减较乘除来说肯定更简单,这种难度差异可在第二部分来划分。
第二部分难度是操作数的大小,操作数越大,计算难度越大,如果同样的操作数,它遇到乘除后得到的值也比遇到加减时变化更大,而这两个因素,都会最终体现在答案的分子和分母上,由于操作数的随机性,分子和分母能约分化简至很小的可能性很低。通过分子+分母的值可以体现出操作数的大小和题目中乘除号的数量带来的影响。也就是说,分子+分母的值越大,则表示该题操作数越大,运算符中乘除的个数也更多,即该题难度更高。
这两部分的难度系数,可以这样定义:首先是由运算符定义的难度系数,假设出题时,教师给出了出题数量$n$,第$i$题的运算符个数用${d_i}(i = 1,2,...,n)$表示,则第$i$题运算符的难度系数可表示为
\[difficult\_op = \frac{{{d_i}}}{{\sum\limits_{i = 1}^n {{d_i}} }}\]
其次,是由分子+分母定义的难度系数。由于分子+分母的值有时会达到极大的值,所以考虑将每题的分子+分母的值取为$ln (分子) + ln (分母)$,但分子有时会为负数或0,分母有时会为负数,这些情况会导致$ln ()$函数返回$Nan$,所以将分子、分母的值都取绝对值后再加上3,以此保证$ln (分子) > 1、ln (分母) > 1$,方便后续处理。假设出题数量依旧为$n$,第$i$题答案的分子、分母为$result{\rm{\_}}numerato{r_i}$、$result{\rm{\_}}den{\rm{o}}min{\rm{a}}to{r_i}$,则第$i$题由分子、分母决定的难度系数\[difficult\_fraction = \frac{{lg(abs(result{\rm{\_}}den{\rm{o}}min{\rm{a}}to{r_i}) + 3) + lg(abs(result{\rm{\_}}numerato{r_i}) + 3)}}{{\sum\limits_{i = 1}^n {(lg(abs(result{\rm{\_}}den{\rm{o}}min{\rm{a}}to{r_i}) + 3) + lg(abs(result{\rm{\_}}numerato{r_i}) + 3))} }}\]
再用$scor{e_i}$表示第$i$题的分值,若试题满分为100分,则有\[scor{e_i} = 50(difficult\_o{p_i} + difficult\_fractio{n_i})\]
根据以上设计分析,设计该打分模块需要的函数、类如下:
|
函数名 |
功能 |
|
record_examination |
用于记录试题中运算符个数以及正确结果的分子、分母值 |
|
examination |
用于考试过程中读取用户输入 |
|
calculate_score |
计算考题的分值和考生最终得分 |
|
examination_time |
设计考试时间,并驱动线程完成考试 |
|
timer1 |
继承自threading.Thread类,启动该线程会不断读取用户输入的答案 |
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/26更新
3 代码说明
本程序的关键代码是产生四则运算题目以及计算其结果,它由$test()$函数内的六个函数实现,它们的具体代码如下:
produce_operators:
1 # 生成指定数量的随机运算符
2 def produce_operators(op_num=3):
3 op = [] # 记录生成的op_num个四则运算操作数
4 for i in xrange(op_num):
5 temp = random.choice(['+','-','×','÷'])
6 op.append(temp)
7 return op
通过$andom.choice函数在四种运算符中随机选择一种,以此实现生成指定数量的随机运算符。
produce_numbers:
1 # 生成指定数量的随机整数和随机真分数
2 def produce_numbers(int_num=2,pro_frac_num=2):
3 num = [] # 记录生成的op_num+1个数(包括整数和真分数)
4 for i in xrange(int_num):
5 int_number = random.randint(0,99)
6 temp = Fraction(int_number, 1) # 生成随机整数
7 num.append(temp)
8 for i in xrange(pro_frac_num):
9 denominator = random.randint(1,99) # 分母(分母不为0)
10 numerator = random.randint(0,denominator-1) # 分子
11 proper_fraction = Fraction(numerator,denominator) # 生成随机真分数
12 num.append(proper_fraction)
13 return num
Fraction类可以将分数作为一个整体进行运算,通过random.randint函数产生0~99范围内的随机整数,再利用random.randint产Fraction类的分子和分母,从而得到随机真分数。
produce_problem:
1 # 生成题目
2 def produce_problem(op_num,num,op):
3 problem = []
4 for i in xrange(op_num):
5 number = random.choice(num)
6 problem.append(number)
7 num.remove(number)
8 operator = random.choice(op)
9 problem.append(operator)
10 op.remove(operator)
11 if i == op_num-1:
12 problem.append(num[0])
13 return problem
利用random.choice函数,在produce_numbers随机产生的操作数列表中随机取出一个操作数,再从produce_operators随机产生的运算符列表中随机取出一个运算符,直至将所有运算符和操作数都用尽,即完成出题。
display_problem:
1 # 打印题目
2 def display_problem(problem):
3 record_problem = ''
4 for i in problem:
5 temp = str(i)
6 record_problem = record_problem +temp
7 record_problem = record_problem + '='
8 isrepetition(record_problem + ' ')
9 print record_problem,
10 return record_problem
将produce_problem函数产生的题目转化为字符串并打印至控制台,在打印前,先判断该题目是否重复,若重复,则重新出题。
calculate_result:
1 # 计算题目正确结果
2 def calculate_result(problem):
3 result = ''
4 for i in problem:
5 if type(i) == type(Fraction(2,3)):
6 numerator_temp = i.numerator
7 denominator_temp = i.denominator
8 temp = 'Fraction(%d,%d)'%(numerator_temp,denominator_temp)
9 else:
10 if i == '+':
11 temp = '+'
12 elif i == '-':
13 temp = '-'
14 elif i == '×':
15 temp = '*'
16 else:
17 temp = '/'
18 result = result + temp
19 final_result = str(eval(result))
20 #print result
21 #print final_result
22 return final_result
计算随机生成的试题的正确答案,结果以分数的形式表示。
judge:
1 # 判断答题者答案是否正确
2 def judge(answer,result):
3 if answer == result: # 判断正误
4 print u'回答正确!'
5 return 1
6 else:
7 print u'回答错误!正确答案是: %s'%(result)
8 return 0
判断答题者答案是否正确,正确返回1,错误返回0。
record_message:
1 #记录答题信息
2 def record_message(show_problem,result,answer,judge_result):
3 message = []
4 message.append(str(show_problem) + ' ')
5 message.append(str(result) + ' ')
6 message.append(str(answer) + ' ')
7 message.append(str(judge_result) + ' \n')
8 return np.array(message)
记录本次答题的题目,正确答案,答题者答案,以及judge函数的返回值。
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/28更新
除以上关键代码,还有完成附加功能的函数,它们包括计算正确率的函数,计算套题中每题的分值并输出考生的考分的函数,考试过程中读取用户输入的函数,判断题目是否有不满足题给要求的函数以及产生考题并记录考题信息的函数。它们的具体代码如下:
calculate_accuracy:
1 def calculate_accuracy(): 2 global test_data 3 correct_question_num = 0 4 total_question_num = test_data.shape[0] - 1 5 for i in xrange(total_question_num): 6 correct_question_num = correct_question_num + int(test_data[i,3]) 7 if total_question_num == 0: 8 accuracy = "{:%}".format(0) 9 else: 10 11 accuracy = "{:%}".format(1.0*correct_question_num/total_question_num) 12 test_data[total_question_num, 2] = str(total_question_num) + ' ' 13 test_data[total_question_num, 3] = str(accuracy) + ' \n' 14 #print accuracy 15 #print total_question_num 16 return None
用于计算在test模式下用户答题的正确率。
calculate_score:
1 def calculate_score(): 2 global examination_data 3 print u'正在计算本次考试成绩......' 4 score = np.zeros([examination_data.shape[0],3]) 5 temp0 = examination_data[:, 1:5].astype('float') 6 sum_temp0 = sum(sum(temp0)) 7 temp1 = np.log(abs(examination_data[:,5:7].astype('float'))+3) 8 sum_temp1 = sum(sum(temp1)) 9 for i in xrange(examination_data.shape[0]): 10 score[i,0] = 50*(1.0*sum(temp0[i])/sum_temp0) 11 score[i,1] = 50*(1.0*sum(temp1[i])/sum_temp1) 12 score[i,2] = examination_data[i,8] 13 final_score = 0 14 sum1 = 0 15 for i in xrange(score.shape[0]): 16 print score[i] 17 if score[i,2] == 1: 18 final_score = final_score + score[i,0] + score[i,1] 19 return final_score
根据建立的分值计算模型,计算每题的分值和考生最终得分。
examination:
1 def examination(): 2 global examination_data 3 global message_flag1 4 try: 5 answer = str(raw_input()) 6 if message_flag1 == 1: 7 if ':' in answer: 8 index = answer.find(':') 9 print examination_data[int(answer[:index]),7] 10 examination_data[int(answer[:index]),8] = str(judge(examination_data[int(answer[:index]),7], 11 answer[index+1:].strip() + ' ',0)) + ' \n' 12 print u'(%d)题回答完毕' % (int(answer[:index])) 13 else: 14 print u'请按正确格式输入' 15 else: 16 print u'考试时间到!' 17 except ValueError: 18 print u'请按正确格式输入' 19 except IndexError: 20 print u'请按正确格式输入' 21 return None
读取考生输入的,指定格式的字符串,判断考生对某一题的回答是否正确。
isrepetition:
1 def isrepetition(problem,choose): 2 global test_data 3 global examination_data 4 if choose == 'test': 5 if problem in test_data[:,0]: 6 raise NameError 7 elif choose == 'examination': 8 if problem in examination_data[:,0]: 9 raise NameError 10 return None
判断考题是否会导致分母为0或者之前已经重复产生过。
record_examination:
1 def record_examination(): 2 op_num = random.randint(1, 10) # 随机生成的运算符个数 3 pro_frac_num = random.randint(0, op_num) # 随机生成的真分数个数 4 int_num = op_num + 1 - pro_frac_num # 生成整数个数 5 op = produce_operators(op_num) # 随机生成指定数量的运算符 6 num = produce_numbers(int_num, pro_frac_num) # 生成指定数量的随机整数和随机真分数 7 problem = produce_problem(op_num, num, op) # 生成题目 8 result = calculate_result(problem) # 计算题目正确结果 9 show_problem = display_problem(problem) # 打印题目 10 isrepetition(show_problem, 'examination') 11 add, sub, mul, div = 0, 0, 0, 0 12 for i in problem: 13 if i == '+': 14 add += 1 15 elif i == '-': 16 sub += 1 17 elif i == '×': 18 mul += 1 19 elif i == '÷': 20 div += 1 21 result = Fraction(result) 22 return np.array([str(show_problem) + ' ', str(add) + ' ', str(sub) + ' ', str(mul) + ' ', 23 str(div) + ' ', str(result.numerator) + ' ', str(result.denominator) + ' ' 24 ,str(result) + ' ',str(0) + ' \n'])
记录考题中每题的加、减、乘、除运算符个数,正确结果的分子、分母值,考生是否答对等信息。
output_testdata:
1 def output_testdata(name,control=0): 2 global test_data 3 global examination_data 4 test_data_path = '%s'%(name) 5 f = open(test_data_path,'a') 6 if control == 0: 7 f.write('序号 题目 正确答案 答题者答案 是否正确(0错误 1正确)\n') 8 for i in xrange(test_data.shape[0]): 9 f.write('(%d) '%(i+1)) 10 f.writelines(test_data[i]) 11 f.write('\n') 12 f.close() 13 elif control == 1: 14 f.write('序号 题目 加号数 减号数 乘号数 除号数 正确答案分子 ' 15 '正确答案分母 正确答案 答题者回答(0错误 1正确)\n') 16 for i in xrange(examination_data.shape[0]): 17 f.write('(%d) '%(i+1)) 18 f.writelines(examination_data[i]) 19 f.write('\n') 20 f.close()
在test模式、practice模式或者examination模式下打印相关信息到txt文本。
4 测试运行
程序运行如图4、图5、图6所示,它们分别展示了程序在test模式、practice模式下和examination模式下的运行结果。其中,程序在test模式下,输入时间即开始测试,测试完成后输出测试结果到txt文本,在txt文本的最下方会标出本次测试总共做过的题数以及答题者的正确率;在practice模式下,输入想要完成的题目数量即可开始练习,需要强调的是,输出的文本共5列,各列之间有若干空格,第1列为序号,第2列为题目,第3列为该题正确答案,第4列为答题者答案,第5列判断答题者的答案是否正确,正确则为1,错误则为0。
--------------------------------------------------我是分界线-------------------------------------------------------------
2017/12/28更新
在examination模式下,输入考试题目数量和考试时间等相关信息即可开始答题,命令行会打印好所有考题,按照指定格式输入字符串来回答对应的题目,在这里由于测试需要,当按照指定格式输入字符串时我打印出了相应题目的答案,考试时间到后打印出了每题由运算符定义的分值和分子+分母的值定义的分值(如图6(d)),观察可以发现(注意题目从第0题开始),我正确回答了12、14、15题的答案,第12题的分值为2.55+2.07,对应的题目十分的复杂(运算符的个数多,操作数大),第14题的分值为1.78+1.75,对应的题目中等,运算符的个数中乘除也较少,第15题的分值为0.51+0.41,对应的题目十分简单,这样的实例对比,说明该种四则运算题目的分值定义方法是合理的,真正考试时,examination模式的输出如图6(f)所示。

(a)

(b)

(c)
(d)
图4 test模式运行结果

(a)

(b)

(c)
图5 practice模式运行结果

(a)

(b)

(c)

(d)

(e)

图6 examination模式运行结果
--------------------------------------------------我是分界线-------------------------------------------------------------
5 项目小结
本次个人作业,我投入了很多时间,具体还是对python语言不够熟练,编程的时候遇到了很多的麻烦,所以在编码上浪费了很多时间。另外一个原因就是我一直想使自己的代码能够更加合理一些,能够在以后利用的时候更好的被调用,但是可能习惯问题,整体代码给我的感觉还是太庞杂,没有将功能块逐一的细分。在设计定时、算分的时候,我思考了一些方式,比如定时,设置time.start()和time.end()来判定是否到达时间,不过最后还是决定用线程来实现;在算用户考试总分的时候,运算符个数是一方面,我还思考根据其他内容,每题在一个基础分上做一些加权和变动,但最终还是选择用分子+分母的方式来判定。在实现定时的时候,我遇到了一个问题一直解决不了,就是当控制台在读取用户输入的时候,这时如果时间到了,我不懂怎么中断控制台的读取,最后想用模拟键盘按键的方式来终止控制台的读取,但这是一种折衷的方式,我想问下老师这种情况该怎么处理比较合适。
在本次任务里面,我一直在思考怎么输出数据比较合理,因为我想对该程序的功能进行一定的扩展,最后我选择将examination模式下试题加、减、乘、除的运算符数量以及答案的分子、分母和用户的答题结果作为一种输出(如图6(e)),这样在输出中我就可以得到描述一道试题的6个特征,它们是加、减、乘、除的运算符数量以及答案的分子、分母,我还可以得到一个label,即用户的答题结果,那么根据这些数据,可以用机器学习中逻辑回归的方法训练,以此判定某用户做该题的结果是正确还是错误。比如,经过若干次测验,得到某用户做题的这些数据,经过逻辑回归训练后,当计算机再次生成一套题后,将每题的特征输入训练参数中可以得到如果该用户做该题题目,其答案是否正确,即完成用户做这套题的结果预测,那么可以预测出该用户做本套题的成绩。如果有很多用户都使用这款软件,记录下每个用户的做题数据,经过训练可以得到每个用户做题的训练参数,从而可以对每个用户完成某套试题进行成绩预测,现在我把这些数据存下来,我想下一步实现该软件的预测功能,希望学习QT以后可以设计较好的UI界面。


浙公网安备 33010602011771号