结对编程—四则运算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方法处理文件可以更方便.对于结对编程的伙伴,启鹏对整个流程的构造比较好,而我则善于实现,通过这次结对项目,收获良多.

猜你喜欢

转载自www.cnblogs.com/universityofLeeds/p/9727176.html