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中的异常处理是很重要的机制和代码规范. 一.错误与异常 通常来说程序中的错误分为两种,一种是语法错误,另一种是异常.首先要了解错误和异常的区别和联系. 语法错误比较容易理解 ...
随机推荐
- ACM ICPC 2011-2012 Northeastern European Regional Contest(NEERC)B Binary Encoding
B: 现在有一种新的2进制表示法,要你求出0~m-1的每个数的表示. 规则如下:n 是满足 m<=2n 最小数. 而0~m-1的数只能够用n-1个位和n个位来表示. 对于n个位表示的数来说不能有 ...
- springboot+mybatis+SpringSecurity 实现用户角色数据库管理(一)
本文使用springboot+mybatis+SpringSecurity 实现用户权限数据库管理 实现用户和角色用数据库存储,而资源(url)和权限的对应采用硬编码配置. 也就是角色可以访问的权限通 ...
- java中FastJson的json类型转换
JSON Gson: 来自Google,功能全面.快速.简洁.面向对象.数据传递和解析方便. Jackson:来源FasterXML项目,社区活跃,更新快 解析速度和效率比Gson快,但无法按需解析, ...
- 基于EasyHook实现监控explorer资源管理器文件复制、删除、剪切等操作
一.前言 最近自己在研究一个项目,需要实现对explorer资源管理器文件操作的监控功能,网上找到一些通过C++实现Hook explorer文件操作的方法,由于本人习惯用.NET开发程序,加之C/C ...
- leetcode234 回文链表 两种做法(stack(空间非O(1)),空间O(1))
link: leetcode234 回文链表 方法1, 快慢指针,把前半部分存入栈中和后半部分比较 public boolean isPalindrome(ListNode head) { if(he ...
- Python基本语法_基本数据类型_序列类型详解
目录 目录 序列 序列的标准操作符 切片操作符 一个例子 字符串的连接 序列的功能函数 enumerate 枚举出序列对象的元素 len 获取序列对象的长度 min 取出sequence中的最小值 m ...
- Java ——补充:构造方法 super()与构造方法 无参 有参构造方法 this()与构造方法
参考文章: https://blog.csdn.net/qq_33322074/article/details/86030836 https://blog.csdn.net/HD243608836/a ...
- 【Qt开发】V4L2 API详解 背景知识 打开设备设置参数
www.linuxtv.org下,有篇文档详细讲解了V4L2相关知识和体系结构.是V4L2方面最全面的文档.可以通过它学习V4L2的一些思路和想法. http://www.linuxtv.org/do ...
- 【Linux开发】【Qt开发】ARM QT移植详细步骤教程
ARM QT移植详细步骤教程 米尔SAM9X5和A5D3X上默认的Qt版本是4.5.3,当这个版本的Qt库不能满足实际开发需求时,可通过此方法制定Qt开发.运行环境. 移植的步骤如下: 1.下载新版q ...
- Environment Modules简单使用
Environment Modules简单使用 Environment Modules简介 Typically users initialize their environment when they ...