zoukankan      html  css  js  c++  java
  • Python核心技术与实战——十|面向对象的案例分析

      今天通过面向对象来对照一个案例分析一下,主要模拟敏捷开发过程中的迭代开发流程,巩固面向对象的程序设计思想。

      我们从一个最简单的搜索做起,一步步的对其进行优化,首先我们要知道一个搜索引擎的构造:搜索器、索引器、检索器和用户接口四个部分搜索器,就是俗话说的爬虫,它在互联网上大量爬去各类网站上的内容,送给索引器。索引器拿到网页和内容后会对内容进行处理,形成索引,存储于内部的数据库等待检索。用户接口就是网页和App前端界面。用户同通过接口想搜索引擎发出询问,询问解析后送达检索器;检索器搞笑检索后,再将结果返回给用户。

      在这里我们不将爬虫作为重点,我们假设搜索样本在本地磁盘上,放五个文件

    # 1.txt 
    I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character. I have a dream today.
    
    # 2.txt
    I have a dream that one day down in Alabama, with its vicious racists, . . . one day right there in Alabama little black boys and black girls will be able to join hands with little white boys and white girls as sisters and brothers. I have a dream today.
    
    # 3.txt
    I have a dream that one day every valley shall be exalted, every hill and mountain shall be made low, the rough places will be made plain, and the crooked places will be made straight, and the glory of the Lord shall be revealed, and all flesh shall see it together.
    
    # 4.txt
    This is our hope. . . With this faith we will be able to hew out of the mountain of despair a stone of hope. With this faith we will be able to transform the jangling discords of our nation into a beautiful symphony of brotherhood. With this faith we will be able to work together, to pray together, to struggle together, to go to jail together, to stand up for freedom together, knowing that we will be free one day. . . .
    
    # 5.txt
    And when this happens, and when we allow freedom ring, when we let it ring from every village and every hamlet, from every state and every city, we will be able to speed up that day when all of God's children, black men and white men, Jews and Gentiles, Protestants and Catholics, will be able to join hands and sing in the words of the old Negro spiritual: "Free at last! Free at last! Thank God Almighty, we are free at last!"

    我们先定义一个基类

    class SearchEngineBase(object):
        def __init__(self):
            pass
    
        def add_corpus(self,file_path):     #读取指定文件的内容
            with open(file_path,'r') as fin:
                text = fin.read()
            self.process_corpus(file_path,text)
    #下面两个函数如果在子类里没有重构的话会直接报错
        def process_corpus(self,id,text):
            raise Exception('process_corpus not implemented.')
    
        def search(self,query):
            raise Exception('search no implemented.')
    
    
    def main(search_engine):
    
        #先指定被搜索的路径
        for file_path in ['1.txt','2.txt','3.txt','4.txt','5.txt']:
            search_engine.add_corpus(file_path)
    
        while True:
            query = input(">>>")
            results = search_engine.search(query)print('found {} results(s):'.format(len(results)))
            for result in results:
                print(result)      #只能搜索到文件名

     SearchEngineBase是个基类,可以被各种不同算法的引擎继承,而每个算法都能实现process_corpus()和search()两个函数,就是对应前面所说的索引器和检索器。而main()函数提供搜索器和用户接口,于是一个简单的包装界面就有了。

    下面我们分析下这段代码:

    add_corpus()负责读取文件内容,将文件路径作为ID,连同内容一起送到process_corpus中,

    process_corpus对内容进行处理,然后文件路径为ID,将处理后的内容存下来,处理后的内容就叫做索引(index)

    search给定一个询问,处理询问,再通过索引检索,然后返回。

    然后我们做一个最简单的搜索引擎(只要实现功能就可以)

    class SimpleEngine(SearchEngineBase):
        def __init__(self):
            super(SimpleEngine,self).__init__()
            self.__id_to_texts = {}
    
        def process_corpus(self,id,text):
            self.__id_to_texts[id] = text    #建立一个字典,key=文件名,value=文件内容,把字典传递给search函数
    
        def search(self,query):             #暴力检索
            results = []
            for id ,text in self.__id_to_texts.items():
                if query in text:    #遍历字典
                    results.append(id)
            return results     #调试时忘记加返回值,程序一直报错。
    
    search_engine = SimpleEngine()
    main(search_engine)
    >>>a
    found 4 results(s):
    1.txt
    2.txt
    3.txt
    5.txt
    >>>
    输出

    当我们给定一个字符时,就会有相应的输出。我们来拆开看一下:

    SimpleEngine实现了一个继承SearchEngineBase的子类,继承并实现了process_corpus和search接口,同时也继承了add_corpus函数(其实这个函数也是可以被重写的),因此我们在main中可以直接调取。

    在我们新的构造函数中

    super(SimpleEngine,self).__init__()   #继承父类的函数和属性
    self.__id_to_texts = {}               #初始化新的属性

    新初始化的字典用来存储文件名到文件内容。

    processc_corpus则是把文件内容直接插入到字典中,这里要注意的时ID应该是唯一的,否则相同的ID会覆盖旧的内容。

    search则是直接枚举字典,从中找到要搜索的字符串,如果能找到就将ID放到列表里返回。


      这里插入个分割线,开始了解一下稍微复杂的搜索引擎了!前面的初始版是最简单的方法,但显然是很低效的一种方式:每次搜索后要占用大量的控件,因为搜索函数并没有做任何事情;而每次搜索也要花费大量的时间,因为所有索引库的文件都要被重新搜索一遍,如果把语料的信息量视为n,那么这里的时间复杂度和空间复杂度都应该是O(n)级别的。

      还有个问题,这里的query只能是一个词或者是几个连着的词。如果想搜索多歌词,而他们有分散在文章中的不同位置,前面的简单引擎就没招了!

      最直接的方法,就是把语料分词,看成一个个的词汇,这样就只需要对每篇文章存储它所有的词汇的set即可。根据齐夫定律,在自然语言的语料库里,一个单词出现的频率与它在频率表的排名成反比,呈现幂律分布。因此,语料分词的做法可以大大提升我们的存储和搜索效率。

      我们先来实现一个Bag of Words的搜索模型(词袋模型)。

    import re
     class BOWEngine(SearchEngineBase):
         def  __init__ (self):
            super(BOWEngine,self). __init__ ()
            self. __id_to_words = {}
    
        def process_corpus(self,id,text):
            self. __id_to_words [id] = self.parse_text_to_words(text)
    
        def search(self,query):
            query_words = self.parse_text_to_words(query)
            result = []
             for id ,words in self. __id_to_words .items():
                 if self.query_match(query_words,words):
                    result.append(id)
            return result
    
        @staticmethod
        def query_match(query_words,words):
             for query_word in query_words:
                 if query_word not  in words:
                     return False
             return True
    
        @staticmethod
        def parse_text_to_words(text):
            text = re.sub(r ' [^w] ' , '  ' ,text)          # 使用正则表达式去除标点符号和换行符 
            text = text.lower()                        # 转换为小写 
            word_list = text.split( '  ' )                # 去除空白单词 
            word_list = filter(None,word_list)         # 去除空白单词
            return set(word_list)                      # 返回单词的set 
    search = BOWEngine()
    main(search)
    >>>will to join
    found 2 results(s):
    2.txt
    5.txt
    >>>will Free god
    found 1 results(s):
    5.txt
    >>>
    运行结论

    这里先理解一个概念,BOW Model,即Bag of Words Model(词袋模型),是NPL领域最常见、最简单的模型之一。假设一个文本,在不考虑语法、句法、段落,也不考虑词汇出现的顺序,只将这个文本看成这些词汇的集合。于是相应的,我们把id_to_texts替换成id_to_words,这样就只需要存这些单词,而不是全部文章,也不需要考虑顺序。

    其中,process_corpus()函数调用类静态方法parse_text_to_words,将文章打碎成词袋,放入set后再放到字典中。

     search()函数就稍微复杂一些,我们假设想搜到的结果都在同一篇文章中,那么我们把query打碎得到一个set,然后把set中每一个词和索引中的每一篇文章进行核对,看一下要找的词是否在里面,而这个过程由静态函数query_match负责。这里两个函数都是静态函数,不涉及到对象的私有属性,相同的输入能得到完全相同的输出结果。所以设置为静态,可以方便其他的类来使用。

    可是这样做每次查询时依然需要遍历所有的ID,虽然比起Simple模型已经节约了大量的时间,但是互联网上由上亿个页面,每次都遍历的代价还是表较大。那么要怎么优化呢?能看出来我们每次查询的query的单词量不会很多,一般也就几个,最多十几个的样子,是不是可以从这里下手!再有,词袋模型并不考虑但此间的顺序,但是有些人希望单词按顺序出现,或者希望搜索的单词在文中离得近一些,这种情况下词袋模型就无能为力了!针对这两点我们需要怎么优化呢?下面就是代码

    import re
    class BOWInvertedIndexEngine(SearchEngineBase):
        def __init__(self):
            super(BOWInvertedIndexEngine,self).__init__()
            self.inverted_index = {}
    
        def process_corpus(self,id,text):
            words = self.parse_text_to_words(text)
            for word in words:
                if word not in self.inverted_index:
                    self.inverted_index[word] = []
                self.inverted_index[word].append(id)
    
        def search(self,query):
            query_words = list(self.parse_text_to_words(query))
            query_words_index = list()
            for query_word in query_words:
                query_words_index.append(0)
    
           #如果某一个查询单词的倒叙索引为空,我们就立刻返回
            for query_word in query_words:
                if query_word not in self.inverted_index:
                    return []
    
            result = []
            while True:
                #首先,获得当前状态下所有的倒序索引的index
                current_ids = []
                for idx,query_word in enumerate(query_words):
                    current_index = query_words_index[idx]
                    current_inverted_list = self.inverted_index[query_word]
    
                    #已经遍历到某一个倒序索引的末尾,结束search
                    if current_index >= len(current_inverted_list):
                        return result
                    current_ids.append(current_inverted_list[current_index])
    
                #如果current_id的所有元素都一样,表明这个单词在这个元素对应的文档中都出现了
                if all(x == current_ids[0] for x in current_ids):
                    result.append(current_ids[0])
                    query_words_index = [x+1 for x in query_words_index]
                    continue
    
                #如果不是就把最小的元素加1
                min_val = min(current_ids)
                min_val_pos = current_ids.index(min_val)
                query_words_index[min_val_pos] +=1
    
        @staticmethod
        def parse_text_to_words(text):
            text = re.sub(r'[^w]',' ',text)         #使用正则表达式去除标点符号和换行符
            text = text.lower()                       #转换为小写
            word_list = text.split(' ')               #去除空白单词
            word_list = filter(None,word_list)        #去除空白单词
            return set(word_list)                     #返回单词的set
    
    
    search_engine = BOWInvertedIndexEngine()
    main(search_engine)

      首先来说这个代码是比较朝纲的了,这次的算法不需要完全理解,只是配合这个例子来讲解面向对象编程是如何把算法复杂性隔离开,而保留其他的接口不变。通过这段代码我们可以看到,新模型继续使用之前的接口,仍然只是在__init__()、process_corpus()和search()三个函数进行修改。

      这也是大公司里团队协作的一种方式,在合理的分层设计后,每一层的逻辑只需要处理好分内的事情就可以了。在迭代升级我们的搜索引擎内核是,main函数、用户接口没有任何改变。

      继续看代码,我们注意到开头的Inverted Index。这是一种新的模型Inverted Index Model,即倒序索引。这是一种非常有名的搜索引擎方法。

      倒序索引,就是说这次反过来,在字典里按照word->id的方式来存储。于是在search的时候,我们只需要把想要的query_word的几个倒序索引单独拎出来,然后从这几个列表中找共有的元素,那些共有的元素,即ID,就是我们想要查询的结果。这样就避免了将所有index过滤一遍的尴尬。

      而search()函数就是根据query_words拿到所有的倒序索引,如果拿不到,就表示有点query word不在任何文章中,直接返回空,拿到之后,运行一个’合并K个有序数组‘的算法,从中拿到我们想要的ID。这里用的算法还不是最优的,最优的写法是哟弄个最小堆来存储index。有兴趣的可以了解一下,这里就不详述了。

      第二个问题,如果我们想要实现搜索单词按顺序出现,或者希望搜索的单词在文中离得近一些要怎么办呢?

      我们需要在Inverted Index上,对于每篇文章也保留单词的位置信息,这样一来,在合并操作的时候做一定的处理就好了。


    最后讲一下LRU和多重继承

      到了这一步我们的搜索引擎就可以上线了,但是随着越来越多的访问量(QPS),服务器有些不堪重负了,经过一段时间的掉要我们发现大量重复性的搜索占据了90%以上的流量,于是我们决定为这个搜索引擎加一个大杀器——缓存。

    import pylru
    class LRUCache(object):
        def __init__(self,size=32):
            self.cache = pylru.lrucache(size)
        def has(self,key):
            return key in self.cache
    
        def get(self,key):
            return self.cache[key]
    
        def set(self,key,value):
            self.cache[key] = value
    
    class BOWInvertedIndexEngineWithCatch(BOWInvertedIndexEngine,LRUCache):
        def __init__(self):
            super(BOWInvertedIndexEngineWithCatch,self).__init__()
            LRUCache.__init__(self)
    
        def search(self,query):
            if self.has(query):
                return self.get(query)
    
            result = super(BOWInvertedIndexEngineWithCatch,self).search(query)
            self.set(query,result)
    
            return result
    search_engine = BOWInvertedIndexEngineWithCatch()
    main(search_engine)

      我们开始通过LRUCache定义了一个缓存,并可以通过继承这个类来调用其方法。LRU缓存是一个非常经典的缓存种类,这里为了简单我们直接调用pylru包,它符合自然界的局部性原理,可以保留最近使用过的对象,而逐渐淘汰掉很久未被使用的对象。所以在search函数中我们先用has()判断是否在缓存中,如果在就直接调用get()来获取,如果不在再重新搜索,返回结果后一并送入缓存。

      在BOWInvertedIndexEngineWithCatch这个类离我们是多重继承的方法继承了两个类。多重继承有初始化方法有两点要注意

    第一是用下面的代码直接初始化该类的第一个父类

    super(BOWInvertedIndexEngineWithCatch,self).__init__()

    不过这种方法要求继承链的顶层父类必须继承object

    这里插一句,我记得python3里好像是可以不用的(涉及到经典类和新式类,可以搜索了解一下),并且可以去掉类名,这么写

    super().__init__()

    第二,对于多重继承,如果有多个构造函数需要调用,我们必须用传统的方法调用用各个类的构造函数

    LRUCache.__init__(self)

      其次我们还可以强行调用父类的函数,我们在子类里已经重构了search函数,但是还想调用父类的search函数,就用下面的方式强行调用。

    result = super(BOWInvertedIndexEngineWithCatch,self).search(query)

    最后留一个问题:私有变量可以被继承么?

    class A():
        def __init__(self):
            self.__a = 'A的私有变量a'
            self.b = 'b'
        def fun(self):   
            return self.__a  #通过函数返回私有变量的值
    
    class B(A):
        def __init__(self):
            super().__init__()
            print(self.b)
            self.data = self.fun()   #间界获取私有变量的值
            print(self.data)
    
    b = B()

    这样就可以了!

  • 相关阅读:
    Linux系统中日志级别详情
    android测试和iOS测试的区别
    React Router的Route的使用
    React中this.props的主要属性
    2019 年 React 学习路线图(转)
    Failed to execute goal org.apache.maven.plugins:maven-clean-plugin:2.4.1:clean (default-clean) on project
    Maven笔记
    优化Android Studio/Gradle构建(转)
    Java相关查询记录
    自己电脑组一个Wifi热点
  • 原文地址:https://www.cnblogs.com/yinsedeyinse/p/11300695.html
Copyright © 2011-2022 走看看