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()
这样就可以了!
Python核心技术与实战——十|面向对象的案例分析的更多相关文章
- Python核心技术与实战——十九|一起看看Python全局解释器锁GIL
我们在前面的几节课里讲了Python的并发编程的特性,也了解了多线程编程.事实上,Python的多线程有一个非常重要的话题——GIL(Global Interpreter Lock).我们今天就来讲一 ...
- Python核心技术与实战——十六|Python协程
我们在上一章将生成器的时候最后写了,在Python2中生成器还扮演了一个重要的角色——实现Python的协程.那什么是协程呢? 协程 协程是实现并发编程的一种方式.提到并发,肯很多人都会想到多线程/多 ...
- Python核心技术与实战——十四|Python中装饰器的使用
我在以前的帖子里讲了装饰器的用法,这里我们来具体讲一讲Python中的装饰器,这里,我们从前面讲的函数,闭包为切入点,引出装饰器的概念.表达和基本使用方法.其次,我们结合一些实际工程中的例子,以便能再 ...
- Python核心技术与实战——九|面向对象
在搞清了各种数据类型.赋值判断.循环以后如果是从C++.Java语言入手的,就会有一个深坑要过:OOP(object oriented programming):公私有保护.多重继承.多态派生.纯函数 ...
- Python核心技术与实战——十八|Python并发编程之Asyncio
我们在上一章学习了Python并发编程的一种实现方法——多线程.今天,我们趁热打铁,看看Python并发编程的另一种实现方式——Asyncio.和前面协程的那章不太一样,这节课我们更加注重原理的理解. ...
- Python核心技术与实战——十五|深入了解迭代器和生成器
我们在前面应该写过类似的代码 for i in [1,2,3,4,5]: print(i) for in 语句看起来很直观,很便于理解,比起C++或Java早起的 ; i<n;i++) prin ...
- Python核心技术与实战——十二|Python的比较与拷贝
我们在前面已经接触到了很多Python对象比较的例子,例如这样的 a = b = a == b 或者是将一个对象进行拷贝 l1 = [,,,,] l2 = l1 l3 = list(l1) 那么现在试 ...
- 阿里云资深DBA专家罗龙九:云数据库十大经典案例分析【转载】
阿里云资深DBA专家罗龙九:云数据库十大经典案例分析 2016-07-21 06:33 本文已获阿里云授权发布,转载具体要求见文末 摘要:本文根据阿里云资深DBA专家罗龙九在首届阿里巴巴在线峰会的&l ...
- Python核心技术与实战——六|异常处理
和其他语言一样,Python中的异常处理是很重要的机制和代码规范. 一.错误与异常 通常来说程序中的错误分为两种,一种是语法错误,另一种是异常.首先要了解错误和异常的区别和联系. 语法错误比较容易理解 ...
随机推荐
- 移动开发与PC开发区别
移动开发领域与PC 开发得区别,总结为:3低, 3高,3有限.开发移动程序是应该时刻记住这9个限制. 3低 低处理能力 低分辨率 低速的数据传输能力 ...
- IO负载高来源定位pt-ioprofile
1.使用top -d 1 查看%wa是否有等待IO完成的cpu时间,简单理解就是指cpu等待磁盘写入完成的时间:IO等待所占用的cpu时间的百分比,高过30%时IO压力高: 2.使用iostat -d ...
- 【原创实现】C 多线程入门Demo CAS Block 2种模式实现
分Cas和Block模式实现了demo, 供入门学习使用,代码全部是远程实现. 直接上代码: /* ================================================== ...
- Vue知识整理9:class与style绑定
1.v-bind:class:绑定class样式.通过控制isActive变量值来实现是否显示:通过.active样式设置背景颜色. 2.支持普通的class与v-bind绑定样式混合使用: v-bi ...
- Delphi XE2 之 FireMonkey 入门(32) - 数据绑定: TBindingsList: TBindList、TBindPosition [未完成...]
Delphi XE2 之 FireMonkey 入门(32) - 数据绑定: TBindingsList: TBindList.TBindPosition [未完成...] //待补...
- Python学习之==>函数
一.函数是什么: 函数是指将一组语句的集合通过一个名字(函数名)封装起来,要想执行这个函数,只需要调用函数名就行. 二.函数的作用: 1.简化代码 2.提高代码的复用性 3.代码可扩展 三.定义函数: ...
- 2018.03.29 python-pandas 数据透视pivot table / 交叉表crosstab
#透视表 pivot table #pd.pivot_table(data,values=None,index=None,columns=None, import numpy as np import ...
- c++ 调用 sqlcipher
#include <iostream> #include <string.h> #include "sqlite3.h" using namespace s ...
- linux中从一台机器复制文件或目录到另一台机器上linux机器上
本机IP:x.x.x.1需要拷贝的机器IP:x.x.x.2用户名:ssh_user 目的:将本机中source_path路径下的文件或目录拷贝到另一台机器的destination_path/路径下 复 ...
- sql语句小记录
测试过程中,需要去数据库中查询一些结果,比如验证码 常用的是查询 更新比较少用 删除一般不用 sql查询语句的嵌套用法,比较实用 比如in的用法:第一种:查询多个值时 SELECT "栏位名 ...