结对项目

这个作业属于哪个课程 软 件 工 程
这个作业要求在哪里 作 业 要 求
这个作业的目标 学习结对编程 ,代码实现、性能分析、异常处理说明、记录PSP表格

写在前面的后记

“人与人是不能一概而论的,打个比方,若要去打tiger,有的人想的是武松打虎,而有的人想的是极端愤怒+滑铲。” ——— 鲁迅

github:

Our code

Team:

信安一班 曾 鑫 3118005357
信安一班 潘景豪 3118005378

具体实现

Ⅰ、计算式的生成

def que_creation(limit):
    op_amount = random.randint(1, 3)
    op = []      #操作符
    num = 0      #当前计算数
    swap = 0      #反转判断符
    danger = 0      #危险度,用于判断加括号的情况
    time = 0      
    que = get_num(limit)      #当前计算结果,用于辅助判断加括号
    last_question = []      #返回题目的最后算法,用于查重
    question = [change_Fraction(que)]
    for i in range(op_amount):
        op.append(random.choice(op_standrad))      #获得1到3个运算符

    for op_sign in op:
        time += 1
        if time == 1:
            last_question = question
        else:
            last_question = [change_Fraction(que)]

        if op_sign == '+':
            num = get_num(limit)
            que += num
            danger += 1

        if op_sign == '-':
            num = get_num(limit)
            danger += 4
            if que > num:
                que -= num
            elif que < num:
                swap = 1
                if danger >= 5:
                    question = ['('] + question + [')'] #出现连续数个减法时加括号,此时danger在5以上
                    danger = 1
                que = num - que
            else:
                while 1:
                    num = get_num(limit)
                    if que >= num:
                        que = que - num
                        break
                  #加减法由于优先度比乘除低,出现加减法会增加danger值
        if op_sign == '×':
            num = get_num(limit)
            que *= num

        if op_sign == '÷':
            num = 0
            while num == 0:
                num = get_num(limit)
                if num != 0:
                    break
            que = Fraction(que, num)
            
        num = change_Fraction(num)
                   
        if op_sign == '×' or op_sign == '÷':
            if danger >= 1:
                question = ['('] + question + [')']
                last_question = ['('] + last_question + [')']
                danger = 0      #当出现乘除运算且danger不为0时,加括号
                              
        if swap == 1:
            question = [num] + [op_sign] + question
            last_question = [num] + [op_sign] + last_question
            swap = 0
        else:
            question = question + [op_sign] + [num]
            last_question = last_question + [op_sign] + [num]
    # print (question)
    # print (que)
    question = ' '.join('%s' % id for id in question)
    last_question = ' '.join('%s' % id for id in last_question)
    # print ("the whole expression is:",question)
    # print ("the last step is:",last_question)
    return question, last_question

要素如下:

1、生成计算式:使用get_num来获取符合要求的随机自然数 or 真分数,而后根据运算符数量来循环获取操作数,组成算式。

图解:

2、加括号
①当加减运算符后面有乘除运算符时进行加括号操作,由于加减运算符的出现增加了danger值,只需对danger值进行判断即可。
②由于题目不允许任何子式结果为负数,故减法必定为大数-小数,故出现连续多个减法操作也需要加括号,由danger的特殊值判断这种情况。

计算式的计算

第一步:处理数据,用正则把不搭边的符号换成能参加运算的+-*/,同时用括号捋清楚各种情况下运算的先后顺序
(或者想办法把上面的que传下来)

                data = a[1].replace('=','')
                data = data.replace('×', '*').replace(' ', '')
                data = re.sub(r"(\d+'\d+/\d+)", r'(\1)', data).replace("'", '+')#筛选假分数,给假分数左右两边加括号来确保运算的正确性

                data = re.sub(r"(\d+/\d+)", r"(\1)", data)#两个除号的情况确保分数不会被拆开
                # 修复()÷÷的运算逻辑错误
                error = re.compile(r"\)÷\d+÷\d+")
                special = error.findall(data)
                if special:
                    data = re.sub(r"(\d+/\d+)", r"Fraction(\1)", data)
                    data = data.replace('÷', '/')
                else:
                    data = data.replace('÷', '/')
                    data = re.sub(r"(\d+/\d+)", r"Fraction(\1)", data)

第二步:通过eval函数进行计算,计算后将结果转化成想要的格式(此处需要先完成数据的处理,eval对于分数的运算需要固定的Fraction()格式)

    def GetResult(self,address1,address2):
        answers = []
        j = 0
        with open(address1,'r',encoding='utf-8') as x:
            for i in x.readlines():
                j +=1
                a = i.split('.',1)
                if a == ['\n']:
                    break
                data = a[1].replace('=','')
                data = data.replace('×', '*').replace(' ', '')
                data = re.sub(r"(\d+'\d+/\d+)", r'(\1)', data).replace("'", '+')#筛选假分数,给假分数左右两边加括号来确保运算的正确性

                data = re.sub(r"(\d+/\d+)", r"(\1)", data)#两个除号的情况确保分数不会被拆开
                # 修复()÷÷的运算逻辑错误
                error = re.compile(r"\)÷\d+÷\d+")
                special = error.findall(data)
                if special:
                    data = re.sub(r"(\d+/\d+)", r"Fraction(\1)", data)
                    data = data.replace('÷', '/')
                else:
                    data = data.replace('÷', '/')
                    data = re.sub(r"(\d+/\d+)", r"Fraction(\1)", data)

                #print(er)
                result = eval(data)
                result1 = Fraction(str(result)).limit_denominator()
                # 检查结果是否有负数
                if result1 <0:
                    print (result1)
                # 重新化为真分数
                s = result1._numerator
                x = result1._denominator
                z = int(s/x)
                if x == 1 or int(result1)==0:
                    result = str(result1)
                else :
                    result1 = result1 - z
                    result = str(z)+"'"+str(result1)
                answers.append(result)

        with open (address2,'w',encoding='utf-8') as y:
            line = 0
            for i in answers:
                line +=1
                y.write(str(line)+'. '+i+'\n')
        return answers

查重算法

在讲述原理前,不如来康康一个小故事

——2077年2月30日,多云,PM 19:23,乔伊·雷酒吧

  “一杯Threading'Fuzz蒸馏酒。”

  衣着邋遢的牛仔缓步走到吧台前,带着锈色和破口的旧手套漫不经心地搭在亮丽的木桌上。他脱下烟灰沾染的帽子,漫不经心地挂在腰间。

  他的衣着和这间酒吧的情调格格不入,简直要被当作流浪汉撵走————如果不是他的腰间还别着锃亮的左轮。
  
  在这酒吧里,枪也不是什么稀奇货,但连阅历无数的酒保都感兴趣的枪很稀奇。

  “你知道的”牛仔看着酒保,“这枪是智械战争后留下来的。我们的敌人狡猾得很,一瞬间,他们能从原子级别上复制我们,你这一刻的动作,表情,心脏跳动
  的频率,液体的流向,甚至大脑的电信号都一模一样”

  墨色的Threading'Fuzz被端了上来,粘稠得如同油。

  “就像是,邻桌也有一个拿着高脚杯的您,不论神态动作甚至记忆都一样?”酒保问到,“那您最后是怎么分辨敌友的呢?”

  “他们的复制出了可笑的bug,例如说我的左手有伤痕,那么复制品的右手会有一模一样的伤痕,他就像镜子里的我,但镜像不论如何都不可能在几何上与
  我重合。”

高中化学我们了解到一个东西叫做手性分子,虽然它们近似到互为镜像,但是他们的效果却通常大相径庭。
回到原理,x 和 + 作为左结合性的运算符,在逻辑上符合中序遍历的二叉树,但同时因为 + 和 x 符合交换律,每个双亲节点的左右子树可以互换(即a+b=b+a,ab=ba),
所以我们可以定义一种类中序遍历:从深度最大的叶子节点开始中序遍历。
这种遍历的特点是,当越靠左的子树深度越大时,这种遍历等价于中序遍历。我们为了方便理解,具体操作时把深度最大的叶子节点通过左右子树互换规则,将其移至左边。

强调:每次互换仅可调换左右子树!

例如说,1+2+3的类中序遍历二叉树为:

而3+(1+2)为:

注意:我们用的是类中序遍历,为了方便理解,这种遍历逻辑上等同于把深度大的叶子节点移动到深度小的叶子节点左边,然后中序遍历。
这么处理后的3+(1+2)为:

结果和1+2+3的类中序遍历二叉树相同,所以两个式子等价。
那么3+2+1的二叉树又是如何呢?

很显然,不论如何交换左右子树,都不可能和1+2+3等价

它是经过右旋后,再进行左右交换的1+2+3二叉树,但经过右旋后已经和原来不再等价。事实上,所有经过左旋/右旋变换的二叉树都不会通过类中序遍历得到同一个式子,就好像两个互为镜像的分子也不是同类。

再如1+2+3+4的类中序遍历二叉树是:

而3+2+1+4的类中序遍历二叉树是:

4+(1+2+3)的类中序遍历二叉树是:

可见,1+2+3+4和4+(1+2+3)等价,但和3+2+1+4不等价

so...本质是什么...

用二叉树的语言描述就是:每一次对子树进行类中序遍历的结果都相同(两个二叉树可以通过交换左右子树变换成对方)。

用算式的语言描述就是:每一次计算的子式都相同

  例如1+2+3的计算顺序为:1+2+3 → 3+3 → 6,3+(2+1)→ 3+3 → 6,故相同。
  而3+2+1 → 5+1 → 6,故不同。

根据上方代码的last_question变量,可以得出每一次计算的子式,通过对比便可避免生成相同的式子。
为了简化代码,最后我们使用了简化版的思路:即只对比最后一次计算式,运算符个数以及运算的数字是否相同,相同即认为是重复。

  事实上这样会有极小概率把不同判为相同,例如1+2+3+4与3+2+1+4属于不同的式子,在这个算法中被认为是相同的式子,但简化的好处有两个,第一是
  判错概率极低,必须最后一次计算式,运算符个数以及运算的数字全部相同,在随机生成整数与真分数的程序中概率极低,第二是即使出错,也并不会生
  成重复的式子,简化了算法的同时依旧符合要求,且和不简化的算法有着99.99%以上的拟合度(根据概率计算),可以接受。

代码如下:

一.先定义好结构体,让每个题目带有问题、操作数、操作符、化简式等四个属性

class Feature:
    def __init__(self,string,symbol):
        number,op = self.information(string)
        self.problem = string           #问题
        self.sym = self.turn(symbol)    #最后化简式
        self.num = '-'.join(number)     #操作数
        self.op = ''.join(op)           #操作符


    def information(self,string):
        number=[]
        op_list = []
        sym = string.replace("×", "*").replace(" ", "").replace("=", '').replace('÷', '/')

        # 生成操作数的数组:
        # 插入假分数
        data_list = re.findall(r"(\d+'\d+/\d+)", sym)
        sym = re.sub(r"(\d+'\d+/\d+)", ' ', sym)
        # 插入真分数
        data_list += re.findall(r"(\d+/\d+)", sym)
        sym = re.sub(r"(\d+/\d+)", ' ', sym)
        # 插入整数
        data_list += re.findall(r"(\d+)", sym)

        for x in data_list:
            if "'" in x:
                y = x.replace("'", '+')
                number.append(str(round(eval(y), 3)))
            else:
                number.append(str(round(eval(x), 3)))

        number.sort()
        #print(number)
        #生成操作符数组
        op = re.compile(r"[+\-*/]+")
        op_list = op.findall(sym)

        return number,op_list

    def turn(self,symbol):
        data_list ,op = self.information(symbol)
        return ' '.join(data_list+op)

二.循环查询题目是否在题目集内

def check(Feature,list1=[]):
    b = Feature
    for i in list1:
        if b.num ==i.num and b.sym ==i.sym and b.op == i.op:
            #print("题目重复")
            return False
        else:
            continue
    return True

性能分析和测试

性能分析与效能改进


优化了check算法之后,哪怕生成10000道题目check的占用时间也不会很长
这部分的改进详情(改进思路与分析等)可以去上面check算法部分查看。
额。。。当然比不上把结果相同的式子去掉的算法来得快就是了。。。但是。。更加符合要求对吧。。
源代码中有一些注释掉的代码,也是多次优化的结果
由于main.py -n -r 与main.py -e .txt -a .txt 调用的代码不同,而且无法完全覆盖异常处理,故不计算覆盖率。

测试

第一、二个需求:

第三、四、五个需求:
这三个需求皆在que_creation(limit)函数中实现(代码在博文开头):
第三个需求对应减法部分,只有减法可能产生负数,故当被减数小于减数时,两者会互换(swap = 1),从题目生成也可看到不存在出现小的数减去大的数这种情况
第四个需求取决于真分数的定义,虽然本身带分数属于假分数,但是根据作业要求中的这句话:

  其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。

可以认为带分数也属于真分数范围内,从现实上讲,也没有必要让被除数永远大于除数(例如8÷6=?),故已正确做到
第五个需求对应函数中的代码op_amount = random.randint(1, 3)

第七个需求:
以上面生成的100题为例:

第八个需求:
如图:

第九个需求(附加题)

十个测试用例与解析(测试用例及答案来源于上面生成的10000道题目):
1、加减法的正确性

2、乘除法的正确性

3、乘除法优先

4、括号优先

前四个属于基本的运算正确性验证
5、减法不生成负数(题目需求)

如果8-1/3作为被减数,小于9,则9和8-1/3互换,并且加上括号
6、若结果不为整数,那么结果为真分数或带分数

(可以正确生成真分数和带分数)
7、带分数乘法(带分数可以正确表示与运算)

8、查重不直接排除结果相同的式子,同时也不会生成相同的题目(后者需要结合整个文档)

9、连续的减法与除法逻辑判断
10、带括号的连续减法与除法(9、10都是证明不符合交换律的运算符运算是否正确,以及括号是否正确生效)

PSP

注:时间为两人合计

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 15
Estimate 估计这个任务需要多少时间 10 10
Development 开发 300 500
Analysis 需求分析 (包括学习新技术) 60 100
Design Spec 生成设计文档 20 20
Design Review 设计复审 20 30
Coding Standard 代码规范 (为目前的开发制定合适的规范) 10 10
Design 具体设计 40 60
Coding 具体编码 160 120
Code Review 代码复审 60 200
Test 测试(自我测试,修改代码,提交修改) 60 60
Reporting 报告 90 240
Test Repor 测试报告 20 60
Size Measurement 计算工作量 20 10
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 60 60
合计 950 1495

个人小结

潘景豪:此次结对编程我实现的是运算、查重模块,虽然写的时候很顺利,但回头代码审查时又会有很多莫名其妙的bug,问题结果都出在了四则运算的优先级上。总的来说是一次很不错的体验,下次结对一定要记得多多沟通

曾鑫:刚开始做结对项目时,我们并没有交流太多,只是把实现逻辑拆成数个结构,规定好每个结构的返回值。思路不同导致有部分代码功能重复,但是又在各自的代码里承担不可替代的工作,很难合并。我认为结对项目进行中要及时和频繁地进行交流,最好一同建立详细的文档来帮助编程,才可以提高代码的效率。不过后续我们之间的反馈和交流多了很多,虽然各自想法不同但是最后我们能互相学习和结合各自的优点,总体上比单人编程要快。

写在后面的后记

“噢,那可真是不可思议。”酒保惊叹道,“我们智械就不会犯这种低级错误。”

牛仔把名为Threading'Fuzz的分馏石油一饮而尽“所以脆弱的碳基生命被取代了。你看外面的烟尘,那是巨神般的机械工厂运作的证明,就像这座小镇剧
烈跳动的炉芯”

“若是碳基生命,别说欣赏这种轰鸣,可能吸入一点烟尘呼吸系统就会Error”

暮光渐渐黯淡,乔伊·雷酒吧慢慢地被沃伦姆德的薄雾掩入夜色,只有远处的庞大工厂如同不夜的京城一般灯火九重天。

(Threading'Fuzz这个词neta自fuzz测试和threading测试)

posted @ 2020-10-12 05:45  kishi_kano  阅读(212)  评论(0)    收藏  举报