目录
软件工程结对项目:Python实现wc程序
结对项目Github地址
项目成员
- 刘志豪 3117008744
- 谭万钏 3117008747
项目要求
说明
实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。
自然数:0, 1, 2, …
真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …
运算符:+, −, ×, ÷
括号:(, )
等号:=
分隔符:空格(用于四则运算符和等号前后)
算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
四则运算题目:e = ,其中e为算术表达式。
需求
使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10
将生成10个题目。
使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
每道题目中出现的运算符个数不超过3个。
程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
四则运算题目1
四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
- 答案1
- 答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
程序应能支持一万道题目的生成。
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e
.txt -a .txt 统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 50 |
· Estimate | · 估计这个任务需要多少时间 | 40 | 50 |
Development | 开发 | 1590 | 1995 |
· Analysis | · 需求分析 (包括学习新技术) | 90 | 80 |
· Design Spec | · 生成设计文档 | 60 | 80 |
· Design Review | · 设计复审 (和同事审核设计文档) | 45 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 25 | 20 |
· Design | · 具体设计 | 120 | 180 |
· Coding | · 具体编码 | 900 | 1200 |
· Code Review | · 代码复审 | 120 | 125 |
· Test | · 测试(自我测试,修改代码,提交修改) | 70 | 60 |
Reporting | 报告 | 40 | 50 |
· Test Report | · 测试报告 | 30 | 50 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1630 | 2045 |
解题思路描述
通过对题目的提取以及重难点的分析,本小组将本项目分成表达式生成模块和答案批改校对模块;在表达式生成模块中,令生成的式子没有重复是十分复杂的,在网上也没有相应的算法,便使用枚举将尽可能多的情况考虑进去。整个项目的解题框架思路如下:
1.生成要求的表达式
- 随机生成数字
- randint函数
- 随机生成符号
- 随机从四种运算符抽取
- 随机应用括号情况
- 组合
- 将运算数、运算符、括号组成表达式
- 查重
- 枚举出不同括号数和不同运算符数下的尽可能多的式子相同情况
- 将生成的式子与对应的情况比对,若相同则返回False;否则返回True
- 真分数转换
- 如果有假分数,则转换为真分数
- 分数用fraction模块
- 计算答案
- 判断括号的情况,采用不同的运算顺序计算出表达式答案
- 格式化输入到文件
- 随机生成数字
2.批改题目,校对答案
- 处理题目
- 提取运算数
- 真分数转换
- 提取运算符
- 根据括号判断运算顺序
- 计算结果
- 提取运算数
- 处理答案
- 比较上一步计算出的答案和answer.txt里的答案
- 返回成绩
- 若比对答案一致:Correct+1
- 若比对答案不一致:Wrong+1
- 处理题目
设计实现
代码组织图



代码分析
查重模块(check.py)
都是先判断字符串是否一样,运算符是否一样,数字是否一样
对于只有一个运算符的,先判断运算符是否一样,若一样再判断数字是否完全一样
'''对于有两个运算符的,若以上都一样,则有以下几种情况: 1.(NoN)oN 2.No(NoN) 3.NoNoN 将他们每个元素放入list中 1.若list长度均为6(无括号情况): 若两个式子都是list1[1]==list2[1] AND list1[3]==list2[3]: if list1[0]=list2[4] and list1[2]=list2[2]: return True else return False else : reurn False 2.若list1长度为6list2长度为8: 若list2[0]='(': if list1[1]等于list2[1]或list2[3] reutnr True else return False 3.若list1长度为8,list2长度为6: 若list1[0]='(': if list2[1]等于list1[1]或list1[3] reutnr True else return False 4.若长度均为8: 判断括号中的数字是否一样即可 对于三个运算符的情况,由于种类较多,若以上都一样,则返回TRUE,否则返回False ''' def isSame (list,salist): numlist1=''.join(list).split('+|-|*|/|(|)') #运算符个数 ops1=''.join(list).split('+|-') for string in salist: if numlist1 == string: return True else : #判断数字是否一样 numlist2=string.split('+|-|*|/|(|)') if len(numlist1)!=len(numlist2): return False else : for i in numlist1 : if i in numlist2: continue else : return False #判断运算符个数是否一样 if (len(list)-len(numlist1)) != (len(string)-len(numlist2)): return False else: if len(list)==6 and len(salist)==6: if list[0]==string[4] and string[2]==list[2] : if list[0]==salist[4] and list[2]==salist[2]: return True else: return False else: return False if len(list) == 6 and len(salist) == 8: if salist[0] == '(': if list[1]==salist[1] or list[1]==salist[3]: return True else: return False if len(list)==8 and len(salist)==6 : if list[0] == '(': if salist[1]==list[1] or salist[1]==list[3]: return True else: return False if len(list)==8 and len(salist)==8: return True def check(list,answer , e_file , a_file): #读取文件 efile=open(e_file,"r") afile=open(a_file,"r") #定义一个list用来存储具有相同结果的式子 salist = [] #先判断库中是否有相同的结果 i=0 j=0 for aline in afile.readlines(): answer=answer.strip() realanswer=aline.split(':')[1] realanswer=realanswer.strip() if answer == realanswer: i+=1 for eline in efile.readlines(): j+=1 if j == i : #提取出式子 eline=eline.split(':')[1] eline=eline.split('=')[0] salist.append(eline.strip()) break return (not isSame(list,salist)) return True
表达式生成模块(Main.py)
import re, os, argparse from random import randint from fractions import Fraction from goto import with_goto import check, grade class EA_gen(): def __init__(self): self.gen_need = 10 self.gen_range = 10 def gen(self): f = open('./exercise.txt', 'a+') f2 = open('./answer.txt', 'a+') f.seek(0) f2.seek(0) f.truncate() f2.truncate() count = 0 while True: try: elist, answer = self.gen_combine() except Exception as e: # 临时作处理:当0位除数 和 负数情况 continue # True表示检查后无重复 if check.check(elist, answer, e_file='./exercise.txt', a_file='./answer.txt') == True: f.write("题目" + str(count+1) + ": " + ' '.join(elist) + ' =\n') if re.search('/', answer): d , n = answer.split('/') if int(d) > int(n): answer = self.__to_fraction(answer) f2.write("答案" + str(count+1) + ": " + answer + '\n') count += 1 if count == self.gen_need: break f.close() f2.close() def gen_combine(self): # 不超过3个运算符 nums_operatior = randint(1, 3) bracket = 0 n1 = self.gen_num() op1 = self.gen_operator() n2 = self.gen_num() elist = [n1, op1, n2] # 两步运算以上 if nums_operatior >= 2: op2 = self.gen_operator() n3 = self.gen_num() elist.append(op2) elist.append(n3) bracket = randint(0,2) # 三步运算 if nums_operatior == 3: op3 = self.gen_operator() n4 = self.gen_num() elist.append(op3) elist.append(n4) bracket = randint(0,4) # 插入括号 if bracket != 0: elist = self.__bracket_insert(elist, bracket) answer = self.__get_answer(elist, bracket) if re.search('-', answer): # 有负号就报错 raise Exception("Negative") return elist, answer def __get_answer(self, elist, bracket): nlist = [] olist = [] flist = [] for i in elist: if re.match(r'\+|-|x|÷', i): if i == '÷': i = '/' # 除号转换 if i == 'x': i = '*' # 乘号转换 olist.append(i) elif re.match(r'\d+', i): nlist.append(i) else: pass for j in nlist: if re.search(r"'", j): f1, f2 = j.split("'") fraction = Fraction(f1) + Fraction(f2) flist.append(fraction) else: flist.append(Fraction(j)) answer = None #根据括号情况计算出答案 if bracket == 0: if len(olist) == 1: answer = eval("flist[0] %s flist[1]" % (olist[0])) if len(olist) == 2: answer = eval("flist[0] %s flist[1] %s flist[2]" % (olist[0], olist[1])) if len(olist) == 3: answer = eval ('flist[0] %s flist[1] %s flist[2] %s flist[3]'%(olist[0], olist[1], olist[2])) if bracket == 1: if len(olist) == 2: answer = eval("(flist[0] %s flist[1]) %s flist[2]" % (olist[0], olist[1])) if len(olist) == 3: answer = eval('(flist[0] %s flist[1]) %s flist[2] %s flist[3]' % (olist[0], olist[1], olist[2])) if bracket == 2: if len(olist) == 2: answer = eval("flist[0] %s (flist[1] %s flist[2])" % (olist[0], olist[1])) if len(olist) == 3: answer = eval('flist[0] %s (flist[1] %s flist[2]) %s flist[3]' % (olist[0], olist[1], olist[2])) if bracket == 3: answer = eval ('flist[0] %s flist[1] %s (flist[2] %s flist[3])'%(olist[0], olist[1], olist[2])) if bracket == 4: answer = eval ('(flist[0] %s flist[1]) %s (flist[2] %s flist[3])'%(olist[0], olist[1], olist[2])) return str(answer) def __bracket_insert(self, elist, bracket): if bracket == 1: elist.insert(0, '(') elist.insert(4, ')') if bracket == 2: elist.insert(2, '(') elist.insert(6, ')') if bracket == 3: elist.insert(4, '(') elist.insert(8, ')') if bracket == 4: elist.insert(0, '(') elist.insert(4, ')') elist.insert(6, '(') elist.insert(10, ')') return elist # 插入括号位置的四种情况 # 1:(NoN)oNoN; # 2:No(NoN)oN; # 3:NoNo(NoN); # 4:(NoN)o(NoN): def gen_operator(self): operators = ['+', '-', 'x', '÷'] return operators[randint(0,len(operators) - 1)] def gen_num(self): #是否用真分数 flag_is_rf = randint(0,1) if flag_is_rf is 1: n = self.gen_fraction() else: n =str(randint(0, self.gen_range - 1)) # 返回的是str类型 return n def gen_fraction(self): denominator = randint(2, self.gen_range) numerator = randint(1, denominator - 1) random_attach = randint(0, 1) real_fraction = str(Fraction(numerator, denominator)) # 调用fraction方法生成真分数 if random_attach != 0: real_fraction = str(random_attach) + "'" + real_fraction return real_fraction def __to_fraction(self, fraction): f = Fraction(fraction) denominator = f.denominator numerator = f.numerator attach = int(numerator / denominator) denominator = numerator - attach * denominator real_fraction = str(attach) + "'" + str(denominator) + '/' + str(numerator) return real_fraction def opt(): parser = argparse.ArgumentParser() # 设置四个选项 parser.add_argument("-n", dest = "need", help = "生成数量") parser.add_argument("-r", dest = "range", help = "生成范围") parser.add_argument("-e", dest = "grade_e", help = "练习文件" ) parser.add_argument("-a", dest = "grade_a", help = "答案文件" ) args = parser.parse_args() return args def main(): args = opt() # #测试用 # args.range = 100 # args.need = 100 # args.grade_e = "exercise.txt" # args.grade_a = "answer.txt" # 这里简化下操作:-n-r输入 或 -e-a输入 两种操作情况。 if args.range and args.need: ea = EA_gen() ea.gen_need = int(args.need) ea.gen_range = int(args.range) ea.gen() elif args.grade_e and args.grade_a: eag = grade.EA_grade() result = eag.grade(args.grade_e, args.grade_a) with open('grade.txt', 'w+') as f: f.write(result) else: print("Please check.") if __name__ == '__main__': main()
答案校验模块(grade.py)
import re from fractions import Fraction class EA_grade(): def grade(self, e_file, a_file): efile = open(e_file, "r") afile = open(a_file, "r") # 定义一个flag记录同行的练习和答案 wrong = [] correct = [] line_flag = 0 # 依次对两个文件里的练习和答案校对 for e, a in zip(efile.readlines(), afile.readlines()) : line_flag += 1 a = a.split(':')[1] a = a.strip() if re.search(r"'", a): a_r, a_f = a.split("'") realanswer = str(Fraction(a_r) + Fraction(a_f)) else: realanswer = str(Fraction(a)) e = e.split(': ')[1] e = e.split('=')[0] nlist = [] olist = [] flist = [] bracket = 0 bracket_after = -1 pattern = re.compile(r"\d+'\d+/\d+|\d+/\d+|\d+") nlist = re.findall(pattern, e) for i in e: if re.match(r'\+|-|x|÷', i): if i == '÷': i = '/' # 除号转换 if i == 'x': i = '*' # 乘号转换 olist.append(i) elif re.match(r'\(', i): bracket_before = e.index(i) bracket_after = e.find(")", -2, -1) if bracket_before == 0 and bracket_after == -1: bracket = 1 if bracket_before != 0 : bracket = 2 if bracket_before != 0 and bracket_after == (len(e)-2) and len(nlist)==4 : bracket = 3 if bracket_before == 0 and bracket_after == (len(e)-2) and len(nlist)==4: bracket = 4 else: pass for j in nlist: if re.search(r"'", j): f1, f2 = j.split("'") fraction = Fraction(f1) + Fraction(f2) flist.append(fraction) else: flist.append(Fraction(j)) cal_answer = None # 分析得出四种情况后,计算答案与answer.txt里的答案校对 try: if bracket == 0: if len(olist) == 1: cal_answer = eval("flist[0] %s flist[1]" % (olist[0])) if len(olist) == 2: cal_answer = eval("flist[0] %s flist[1] %s flist[2]" % (olist[0], olist[1])) if len(olist) == 3: cal_answer = eval('flist[0] %s flist[1] %s flist[2] %s flist[3]' % (olist[0], olist[1], olist[2])) if bracket == 1: if len(olist) == 2: cal_answer = eval("(flist[0] %s flist[1]) %s flist[2]" % (olist[0], olist[1])) if len(olist) == 3: cal_answer = eval('(flist[0] %s flist[1]) %s flist[2] %s flist[3]' % (olist[0], olist[1], olist[2])) if bracket == 2: if len(olist) == 2: cal_answer = eval("flist[0] %s (flist[1] %s flist[2])" % (olist[0], olist[1])) if len(olist) == 3: cal_answer = eval('flist[0] %s (flist[1] %s flist[2]) %s flist[3]' % (olist[0], olist[1], olist[2])) if bracket == 3: cal_answer = eval('flist[0] %s flist[1] %s (flist[2] %s flist[3])' % (olist[0], olist[1], olist[2])) if bracket == 4: cal_answer = eval('(flist[0] %s flist[1]) %s (flist[2] %s flist[3])' % (olist[0], olist[1], olist[2])) except Exception as exception: wrong.append(str(line_flag)) continue if Fraction(realanswer) - Fraction(cal_answer) < 1: correct.append(str(line_flag)) else: # print(line_flag, (realanswer),Fraction(cal_answer), e, bracket, len(nlist), bracket_before, bracket_after) wrong.append(str(line_flag)) # 处理结果,返回输出 correct_result = "Correct:" + str(len(correct)) + " " + "(" + ",".join(correct) + ")\n" wrong_result = "Wrong:" + str(len(wrong)) + " " + "(" + ",".join(wrong) + ")" return correct_result + wrong_result
代码覆盖率
- 用coverage获取代码覆盖率

Module | statements | missing | coverage |
---|---|---|---|
Total | 290 | 54 | 91% |
Main.py | 52 | 39 | 25% |
check.py | 55 | 42 | 24% |
grade.py | 77 | 4 | 95% |
测试
单元测试
测试详情(手动测试)
测试-r -n功能
- 一道题目生成情况并且1以内的数字运算:
Main.py -r 1 -n 1 #在中断输入
题目文件:
答案文件:
- 一万道题目生成情况:
题目文件:  答案文件: 
测试-e -a功能
在答案文件中修改如下三个地方,即将该三个地方的答案改错
用 -e -a功能测试答案错误率
测试结果:
回归测试
回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误的一种测试方法。
回归测试的重心,是以关键性模块为核心。
本项目规模小、功能有限、关键性模块不变更,可以重新调用单元测试观察结果作回归测试。
效能分析
- 采用Python的CProfile工具进行分析
- ncalls:表示函数调用的次数;
tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间;
percall:(第一个percall)等于 tottime/ncalls;
cumtime:表示该函数及其所有子函数的调用运行的时间,即函数开始调用到返回的时间;
percall:(第二个percall)即函数运行一次的平均时间,等于 cumtime/ncalls;
filename:lineno(function):每个函数调用的具体信息;

项目总结与收获
本次项目由Python编写,工具、模块主要有用到:PyQT的QtDesigner窗口设计、re正则表达式模块、glob通配符模块、OptionParser参数解析、unittest用于单元测试、Cprofile用于效能分析等。整体的项目分工并不难,将整个项目分成表达式生成模块、答案生成模块和答案校验模块,其中表达式生成模块中难点需要注意判断生成的表达式是否有相同并将其排除;正因为题目中有了这个不能出现重复题目的要求,为了去重而设计出的算法占了很大的时间复杂度,直接影响了题目生成的效率,这是本项目做的不足的地方,希望后期可以设计出时间复杂度更低的算法以优化生成大量题目的速度。
由于本次项目是第一次结对项目,对队友的擅长领域和代码风格都不太了解,中途遇到过不少难题,但经过一段时间的磨合便很快适应了彼此的风格与节奏。因此,整个结对项目下来,学到了团队之间的磨合与沟通很重要,它直接影响了项目的进度。通过及时有效地沟通可以改进项目的瓶颈。