源代码链接:https://github.com/superwales/Random_test/blob/master/random_test1.0.py
一,psp表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 30 | 60 |
Estimate | 估计这个任务需要多少时间 | 860 | |
Development | 开发 | 540 | 690 |
Analysis | 需求分析 | 课程项目,10 | 暂无 |
Design Spec | 生成设计文档 | 60 | 暂无 |
Design Review | 设计复审 | 60 | 暂无 |
Coding Standard | 代码规范 | 30 | 30 |
Design | 具体设计 | 60 | 90 |
Coding | 具体编码 | 120 | 300 |
Code Review | 代码复审 | 个人项目,自我测试,300 | |
Test | 测试(自我测试,修改代码,提交修改) | 300 | 300 |
Reporting | 报告 | 60 | 60 |
Test Report | 测试报告 | 60 | |
Size Measurement | 计算工作量 | 10 | 10 |
Postmortem&Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | 60 |
合计 | 860 | 910 |
二,解题思路描述
本次个人项目共有四个基本要求:
1,操作数随机生成,包含整数和真分数
2,运算符的种类的顺序随机生成
3,能判断对错并计分
4,生成的题目数量可以控制
以及四个附加要求:
1,运算符的个数随机生成
2,根据题目难度分配分值
3,根据答题时间调整分数
4,防止题目重复
首先考虑基本要求:根据用户输入的题目数量,随机生成字符串,打印出来就能得到一个题目,由于要与用户输入的答案比较,所以应当知道这个字符串的值是多少。而题目是操作数与操作符交错的,只要确定了操作符,然后插入随机的操作数就能得到一个题目。
考虑附加要求1:把运算符存在一个列表中,然后随机生成序号,取出对应的运算符就好
考虑附加要求2:用题目的长短(操作符的个数)和乘除法占的多少来衡量一个题目难易与否,把难度量化,根据难度分配分值,满分默认为100分
考虑附件要求3:首先根据题目多少计算答题时间,然后有两个思路,一是超时就停止答题,二是在规定时间答完正常打分,超时扣分
考虑附加要求4:把所有已生成的题目都存起来,每得到一个新题目,就到题库里去比,如果重复就重新生成一个新题目。
在开始本次项目之前,我只有一定的C和C++基础。在和同学沟通讨论后,我决定自学python,并用python完成本次作业。首先通读了《简明python教程》(Swaroop, C. H . 著沈洁元 译),明确了python的相关编程规范之后,开始了代码的编写。
三,设计实现过程
基本功能点:
用户交互得到题目数量与运算范围:首先由用户决定需要多少题以及操作符的运算范围是多大,使程序的适用性更强
随机生成操作符:把所有的操作符都存在一个列表中,例如【‘+’,‘-’,‘*’,‘/’】,然后随机从中抽取。后来考虑到小学运算的难度,我调整了加减号和乘除号的比例,为2:1。即建立一个【‘+’,‘-’,‘+’,‘-’,‘*’,‘/’】列表,然后从中随机抽取操作符
随机生成操作数:首先获取一个在(1,2)中的随机整数,如果为1,则返回一个整数,如果为2,则返回一个真分数,保证了操作数的随机性。与调整操作符种类比例类似,可以调整操作数的范围来改变返回整数与真分数的比值。返回整数直接在操作数运算范围中随机得到一个就行,如果是返回真分数,可以获取两个随机数。python有一个Fraction类可以进行分数的数值运算和自动约分。
随机得到题目(包含运算符个数随机生成):首先在(1~10)中随机获取操作符的个数,这也确定了题目的长度。然后调用已经得到的随机获取操作符和随机获取操作数的函数,交错得到一道题目。
把题目写入txt文件中:每得到一道新题就写一道新题,便于后续查看与打印。当前只能写题,后续将开发把题目,参考答案和回答答案、每题的对错和对应的分数全部写入,即打印成绩单。
计算题目的答案:这部分困扰我最久。我把一道题目存在一个列表中,列表中的元素是操作数或者操作符。比如:
【‘5’,‘+’,‘1/2’,‘*’,‘4’,‘-’,‘2’】
在没有括号的情况下,乘除运算先于加减。于是我从左到右遍历这个列表,如果有乘除号,那么使用乘除号和前后的两个操作数进行一次乘除运算,把结果赋给前一个操作数,并删除操作符和后一个操作数:
【‘5’,‘+’,‘2’,‘-’,‘2’】
做完所有的乘除法后,就用前三个元素做加减运算,同样把值赋给前一个操作数,并删除操作符和下一个操作数:
【‘7’,‘-’,‘2’】
直到列表只剩最后一个元素:
【‘5’】
这个数就是表达式的答案。
但是我这种有一个很明显的缺点,就是无法处理包含括号的字符串运算。
题目判重:用一个列表存题,每生成一道题目就去题库中比较一次,如果不一样就添加到题库中,如果一样就重新随机生成。
根据用户输入判断答题对错:到这里已经得到了题目和答案,分别用两个列表存储。如果用户的输入与答案相符,输出“回答正确!”,反之输出“回答错误!”
根据题目难易分配分值:首先是量化难易度。回溯到随机操作符和操作数的时候,如果随机得到了一个加减号,则认为难度+1,乘除号,则难度+2;如果随机得到一个整数,则难度+1,真分数,则难度+1.5。在随机生成题目的时候,设置初始难度为0,每得到一个操作数和一个操作符就把难度累加起来,得到一个难度的判据。把难度也可以存在一个列表中。所有的题目可以累加得到一个总难度,根据每一题的难度占总难度的比值就可以换算为百分制的分数。每做对一题,就把获得的分数加起来,得到一个初始分数。当前代码使用整数表示分数,当题目过多时,可能出现每一题都是零分的情况,后续应当修改为浮点数。
根据答题时间调整分数:根据题目难度的不同应当分配不同的时间,我设置了一个系数k表示题目难度与时间(s)的换算比例,默认是1。意思是,当题目总难度为100时,则默认100s内结束答题为正常。如果超时,则使用
grade=(grade*time_limite)/time_use
来调整分数
四,代码说明
算法思路已经在第三部分讲解得比较详细了,这一部分主要用注释来说明。
1,随机生成操作符:
def getoperators():
operatorslist=('+','-','*','/','+','-')#在列表中调整四种运算符出现的比值
operators=random.choice(operatorslist)#从列表中随机选一个元素,保证了随机性
if operators=='*':#乘除号难度赋值为2
length=2
elif operators=='/':
length=2
else:
length=1#加减号难度赋值为1
return operators,length#返回操作符与其难度
2,随机生成操作数:
def getoperands(range):
operandstype=random.randint(1,2)#得到一个随机数,确定返回的数据类型
degree=0#难度初始化为0
if operandstype==1:#返回整数,有三个参数:字符串,用于打印与显示;数值,用于计算;难度,用于分配分数
operands=random.randint(1,range)
operandsvalue=Fraction(operands,1)#整数可以看做是一个分母为1的分数,统一为fraction格式可以方便后面的数值运算
operands=str(operands)#字符串
degree=1#整数难度为1
else:#返回真分数
operands1=random.randint(1,range)#得到两个随机数
operands2=random.randint(1,range)
degree=1.5#真分数难度为1.5
if operands1<operands2:#调整大小,确保分子小于分母,为真分数
operands1,operands2=operands2,operands1
if operands1==operands2:#万一分子分母相等,赋值为1/2
operandsvalue=Fraction(1,2)
operands='(1/2)'
else:
operandsvalue=Fraction(operands2,operands1)
operands=str(Fraction(operands2,operands1))
return operands,operandsvalue,degree#有三个参数:字符串,用于打印与显示;数值,用于计算;难度,用于分配分数
3,随机得到题目:
symbolnumber=random.randint(1,5)#随机得到操作符的个数,题目要求是1~10,这里是1~5,原理一样
question=''#每一题用字符串存
questionstack=[]#用列表存下题目的每一个操作数和操作符,以用自定义的方法计算答案
ans=0#用于后文计算答案,初始化为0
length_ques=0#用于后文分配分值,初始化难度为0
for i in range(1,symbolnumber+1):#根据有多少个操作数,逐一得到操作数与操作符
(op,va,de)=getoperands(ran)
operands=op#传递字符串
value=va#传递数值
(operators,length)=getoperators()
question=question+operands+operators
questionstack.append(value)#生成列表
questionstack.append(operators)
length_ques=length_ques+length+de#难度累加
(op,va,de)=getoperands(ran)
operands=op
value=va
question=question+operands#得到了一道题目
questionstack.append(value)#得到了便于计算答案的题目的列表格式
length_ques=length_ques+de#难度累加
4,计算答案:
condition=0#条件,乘除法计算完毕时变为1
while len(questionstack)>1:#计算至最后一个元素
for i in range(0,len(questionstack)):
if questionstack[i]=='*':#先算乘除,
questionstack[i-1]=questionstack[i-1]*questionstack[i+1]#一次运算并赋值
del questionstack[i]#删除元素
del questionstack[i]
break
elif questionstack[i]=='/':
questionstack[i-1]=questionstack[i-1]/questionstack[i+1]
del questionstack[i]
del questionstack[i]
break
else:
condition=1#表示乘除法已经计算完毕
if condition==1:
if len(questionstack)>1:
questionstack[0]=calculate(questionstack[0],questionstack[2],questionstack[1])#始终在前三位进行运算,意思是从左到右执行加减法
del questionstack[1]
del questionstack[1]
else:
ans=questionstack[0]#最后剩下的元素就是答案
5,题目查重:
questionfile=file('questionlist.txt','w')#准备把题目写入txt
#根据输入的题目个数生成题目清单,每一个新生成的题会与现有题进行比较,重复不则重新生成
#将生成的题目写入文件中,便于查看与打印
while len(questionlist)<questionnumber:#用户输入的题目数量
(question,ans,length)=getquestion(ran)#随机得到一道题目
cond=0
for element in questionlist:#在现有列表中去比较
if element==question:#如果题目已经存在,那么重新生成一道题
cond=1
if cond==0:#cond=0表示题目无重复
questionlist.append(question)#题目用列表存
anslist.append(ans)#答案用列表存
lengthlist.append(length)#每一题的难度用列表存
totalscore=totalscore+length#所有题的总难度,为后文分配分值准备
questionfile.write(question+'
')
questionfile.close()
print '题目已经生成完毕!'
6,根据权重分配分值:
for i in range(0,len(lengthlist)):
scorelist.append(round(lengthlist[i]*100/totalscore)) #totalscore是总难度,每一题的难度占比转换为百分制的分数
7,判断对错:
for i in range(0,questionnumber):#总共有questionnumber道题
print'第',i+1,'题是:'
print questionlist[i],'='
print'分值为',scorelist[i],'分'
print '答案是:',anslist[i]
ans_user=raw_input('请输入你的答案:')
print '您的答案是:',ans_user
if ans_user==str(anslist[i]):
print'回答正确!'
grade=grade+scorelist[i]#答对的分值累加起来,得到初始成绩
else:
print'回答错误!'
8,根据时间调整分数:
time_start=time.time()#答题前开始计时
*************************
time_end=time.time()#答题结束停止计时
time_use=time_end-time_start#实际花费时间
#这里设置一个题目权重与时间花费的关系k,默认为1
k=1
if time_use<totalscore*k:#在规定时间内,分数不做调整
print'答题时间符合要求!'
grade=grade
else:
print;'答题超时!'
grade=round(totalscore*grade/time_use)#答题超时后,根据超出时间的多少调整分数
print '您本次的得分为:',grade,'分'
五,测试运行
展示两个运行结果
一:
本次运行时,首先输入了题目的个数和运算范围,然后开始答题。为了方便测试我在屏幕中输出了答案。每一题都标明了分值,在作答后直接判断对错。答题结束后显示时间符合要求,结果全对,成绩为100分。
二:
而在本次运行中,所有题目依然全部答对,但是超出了答题时间太多,最终得分只有51.0分。
同时,题目已经被写入到txt文件里:
六,项目小结
总的来说,本次项目的要求均已完成。由于本次项目不复杂,在编代码之前就能够很清晰的想清楚该做些什么。遇到的最大的困难是对python语言不熟悉,很多功能不清楚。
psp表格已经在博客的开头给出了,代码复审和单元测试还没有进行,后续会学习,尽快培养出良好的软件开发习惯。
目前的成果还存在很多漏洞或者说希望改进的地方:
1,分数计算有问题。由于在分配分值的时候进行了取整,引入了较大的误差。可能出现规定时间内每题都做对了,可是分数不是100分的情况。
2,根据时间调整分数的模型做的不够合理,在超出时间后,分数衰减得很快
3,生成的题目不包括括号
4,程序运行界面太不友好
等等等等。
因此我会继续改进我的代码。之前有一个想法是做一个可视化的界面出来,就类似于驾考宝典这种,一开始确定相关参数,然后显示出一道题目,回答之后直接判对错,点击下一题继续等等。但是在学习用python开发界面的时候遇到了一些问题,所以想法暂时搁置了。
在12月25日课上,我读一本python的教程,里面有一句话说“python处理100万字符的txt文档”,大概的意思就是这样。这句话突然启发了我,我为什么不能直接做一个题库,然后直接从题库里抽题呢?这样还可以实现和何老师交流时提到的“题目主观难度判断”,有关题库的相关尝试我将写在下一篇博客中。