结对编程—四则运算in python(梁朗章,林启鹏)
github address:https://github.com/langliang/-arithmetic.git
PSP:
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
30 |
30 |
· Estimate |
· 估计这个任务需要多少时间 |
20 |
20 |
Development |
开发 |
540 |
600 |
· Analysis |
· 需求分析 (包括学习新技术) |
60 |
90 |
· Design Spec |
· 生成设计文档 |
30 |
30 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
30 |
30 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
15 |
20 |
· Design |
· 具体设计 |
30 |
30 |
· Coding |
· 具体编码 |
540 |
600 |
· Code Review |
· 代码复审 |
30 |
30 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
60 |
90 |
Reporting |
报告 |
120 |
90 |
· Test Report |
· 测试报告 |
30 |
30 |
· Size Measurement |
· 计算工作量 |
20 |
20 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
20 |
20 |
合计 |
|
1575 |
1730 |
三.效能分析:
我在效能分析上大约花费了将近3个小时,主要思路为:python语言中列表的传递都是引用传递,在函数中改变列表,势必会对原列表也造成不可逆转的改变,所以如何合理规划函数调用的顺序很关键。过多使用切片,会造成空间时间效率低,不使用切片就要选择其他方法保护原列表,我在get_answer函数中对操作数列表有delete操作,最后operands列表只剩最终计算结果,所以选择先将问题写入exercise文件,再get answer,就避免了切片的开销。在对operator优先级的排序中,本来使用较为简单的lamda函数,但是为了提高效率,改用numpy库中的numpy.lexsort同时对两个列表进行排序,加快了速率。
使用ipython shell进行效能分析:
生成一万道题和答案,生成十次,平均1.57秒一次:
生成100万道题和答案:
以上的测试只是对整个程序的性能测试。由于是在虚拟机中测试,速度会比较慢,所以对每行代码分析所占时间比才最为重要
通过cprofiler对show_questions_and_answers函数每个调用进行效能分析,由于该函数通过调用其他函数,实现了所以的问题答案生成和写入功能,因此对该函数每个调用进行分析就能知道性能瓶颈
here we go:
通过分析,在写入问题时将问题操作数转化为string花费较多时间,还有random.randint方法,前者可通过较少不必要的str()操作,后者可用numpy里面的方法代替或者减少不必要操作。
好像看的还不是很清楚,使用可视化组件gprof2dot和graphviz来更清楚的分析:
从图像可知,生成操作数的方法居然占到了34%,而我原本以为较占用时间的生成答案的方法只占到20%,令我非常意外,而34%里面竟然有将近24%是random.randint方法占据的,可见此方法的效率之低,将原本整数或小数的标记符的random.randint改为random.choice
然后:
生成操作数的方法占比减少1%,虽然不多,还是有用的XDD。
接下来还可以对get——question方法减少str()操作来提高效率,在一顿操作后,10000道题的时间减少为1.43秒!
四.设计实现过程:
顺序:
def operators_list():随机生成1-3个操作符
def generate_operands(num):根据生成操作符的个数随机生成n+1个操作数
def get_answer(op_list, operands, index_list, priority):根据生成的操作符和操作数,和操作符的优先级(先按操作符优先级,再按索引号),计算结果
def get_question(op, operands):根据操作符和操作数,生成问题的字符串,以写入到文件
def tran_to_proper_fraction(a, fract):将大于一的分数转化为真分数
def show_questions_and_answers(number):通过重复调用 get_answer和get_question方法,得到n个问题和答案,并写入到相应文件中
def check_answers(exefile, ansfile):检查答案是否正确
五.代码说明:
# 生成操作符,并先根据操作符本身的优先级排序,然后再根据索引号排序,返回优先级索引列表和操作符个数,操作符列表,操作符原索引列表(用来确定操作符对应的操作数)
1 # In[13]: 2 # A method generating a operators list 3 4 def operators_list(): 5 # operators number should range from 1 to 3 6 num_op = random.randint(1, 3) 7 # a list stored random operators 8 arithmetic_operators_list = [] 9 index_list = [] 10 priority_list = [] 11 for i in range(num_op): 12 op = random.choice(['+', '-', '*', '/']) 13 if op == '*' or op == '/': 14 priority_list.append(0) 15 else: 16 priority_list.append(2) 17 index_list.append(i) 18 arithmetic_operators_list.append(op) 19 priority = numpy.lexsort((index_list, priority_list)) # sorted by priorities and then indexes 20 return num_op, arithmetic_operators_list, index_list, priority
生成答案:
# 根据优先级索引列表,确定先处理的操作符,根据操作符原索引列表,确定其对应的操作数,例如索引号为【2】的操作符,对应的操作数索引号为【2】和【3】,
其后删除使用过的操作数,并把刚刚得到的中间结果插入操作数列表中,例如【2】【3】的操作数,插入到【2】中,迭代n次处理完所有操作符后,操作数列表就只剩下最终结果,返回即可
1 def get_answer(op_list, operands, index_list, priority): 2 outcome = Fraction(0) 3 for i in range(len(op_list)): 4 index = index_list[priority[i]] # currently processing operator according to priority 5 if op_list[priority[i]] == '+': 6 outcome = operands[index] + operands[index+1] 7 elif op_list[priority[i]] == '-': 8 outcome = operands[index] - operands[index+1] 9 elif op_list[priority[i]] == '*': 10 outcome = operands[index] * operands[index+1] 11 else: 12 outcome = operands[index] / operands[index+1] 13 del operands[index] # delete the used operands 14 del operands[index] 15 for j in range(len(index_list)): # index minus 1 16 if index_list[j] > index: 17 index_list[j] -= 1 18 operands.insert(index, outcome) # intermediate result stored to the list as an operand 19 return operands[0]
转化成真分数:
# 在将问题写入到文件时,要将形如3/2这样的分数转换为1'1/2这样的形式,为了不影响后续的计算,操作数使用切片深度复制。
1 def tran_to_proper_fraction(a, fract): 2 b = fract.numerator // fract.denominator 3 k = str(b) + "'" + str(Fraction(a, fract.denominator)) + ' ' 4 return k
将问题和答案写入到文件中:
# 循环生成n个问题和答案,并写入到文件中
1 ef show_questions_and_answers(number): 2 ef = open('Exercises', 'w') 3 af = open('Answers', 'w') 4 for i in range(number): 5 number_op, operators, a, b = operators_list() 6 operands_list = generate_operands(number_op) 7 question = get_question(operators, operands_list) 8 # write the question to exefile 9 ef.write('%d. ' % i) 10 ef.write(question) 11 ef.write(' =? ') 12 ef.write('\n') 13 # write the answer to ansfile 14 answer = get_answer(operators, operands_list, a, b) 15 af.write('%d. ' % i) 16 af.write('%s' % str(answer)) 17 af.write('\n') 18 # close files 19 ef.close() 20 af.close()
检查答案:
1 def check_answers(exefile, ansfile): 2 ef = open(exefile) 3 af = open(ansfile) 4 e_lines = ef.readlines() 5 a_lines = af.readlines() 6 right_num = [] 7 wrong_num = [] 8 for i in range(len(e_lines)): 9 # get your answers 10 your_answer = e_lines[i].split('?')[-1].strip() 11 # get right answer 12 right_answer = a_lines[i].split('.')[-1].strip() 13 if Fraction(your_answer) == Fraction(right_answer): 14 # marking right answers number 15 right_num.append(i) 16 else: 17 # marking wrong answers number 18 wrong_num.append(i) 19 right_num = tuple(right_num) 20 wrong_num = tuple(wrong_num) 21 ef.close() 22 af.close() 23 24 # to write down your grade 25 26 with open('grade.txt', 'w') as grade: 27 grade.write("correct: %d " % len(right_num)) 28 grade.write(str(right_num)) 29 grade.write('\n') 30 grade.write('wrong: %d' % len(wrong_num)) 31 grade.write(str(wrong_num)) 32 grade.write('\n')
检查是生成问题还是检查答案:
# 设置标记符,False则生成问题和答案,若检查答案,则吧标记符改为True
1 c_or_a = False 2 3 4 # In[18]: 5 6 7 if options.exercises != '' and options.answers != '': 8 c_or_a = True 9 check_answers(options.exercises, options.answers) 10 11 12 if not c_or_a: 13 if options.range <= 1: 14 try: 15 sys.exit(0) 16 except SystemExit: 17 sys.stderr.write('You must give a proper range value! Please try again!\n') 18 else: 19 show_questions_and_answers(options.number)
六.测试运行:
由于题目要求生成r内,但不包括r的自然数或分数,而分母也要小于r,对于1,分母为整数的情况下,分母小于1,整个分数又小于1的分数不存在,所以r=1会被当成异常来处理
n的默认值被设计为1
异常处理:
不输入r:
输入不合适的r:
usage:
其他测试:
生成r=10,n=100
检查答案:
问题:
答案:
成绩:
生成一万道r=6的题目:
无论是 1000道还是1道,r的任何合理取值,都能生成正确答案,都能正确得出成绩,证明程序逻辑正确
七.项目小结
结对中两人的合作非常重要,特别是如何分工,一人负责算法和程序逻辑,另外一个负责实现的数据结构,可以大大提高效率,例如是用元祖列表存储还是矩阵,用eval计算结果还是用迭代.另外对程序设计语言的理解也非常重要,python语言中for item in list的循环,对item的修改不会修改到原list中的对应项,这个问题困扰了我一个多小时.另外网上的资源也十分有帮助,通过上网搜索,我获得了很多跟矩阵和numpy中二维数组的操作方法,还有用with open方法处理文件可以更方便.对于结对编程的伙伴,启鹏对整个流程的构造比较好,而我则善于实现,通过这次结对项目,收获良多.