【读书笔记】《Python_Cookbook3》第一章:数据结构和算法

 
Python提供了多样化有用的内建数据结构,例如列表、集合、字典。大多数时候,这些结构的使用比较简单,而后,一些关于搜索、排序、过滤的常见问题常常出现。本章节的目标是讨论常见的数据结构,以及涉及到的数据算法。另外,介绍模块集合中多样的数据结构
 
1.1将序列解析成不一样变量
问题:有N个元素的集合或列表,想要将它解析成N个变量
解决方法:
任何序列(或者迭代)能够经过简单的运算解析成不一样的变量。要求变量的数量和序列的结构相匹配(个数等一致),以下面的例子
【元祖】
【列表】
若是变量数量和元素个数不符,将会报错,好比:
讨论:不是只有元祖和列表能够解析,任何对象发生迭代时均可以用解析。包括字符串、文件、迭代器、生成器。例以下面的例子:
解析时,有可能你不想要解析全部的内容,Python为此没有提供特殊的语法,可是你能够用一个无用的变量名来替换它。例如:
可是注意:这个不要的变量名字必定不要和已经使用的变量名相同,不然数据会被覆盖
 
1.2任意长度的迭代器元素解析
问题:你须要解析迭代器的N个元素,可是迭代器的实际元素可能比N长,会引发“too many values to unpack”的异常
解决办法:
Python的*表达式能够解决这个问题。举个例子,年级结束时,你决定计算你课程分数,去掉第一次和最后一次的分数,取平均值。若是只有4次做业,你能够简单的解析成4个变量,可是若是有24个做业呢?一个*表达式能够简单的解决这个问题:
另外一个例子,假如你有一个用户记录,包括姓名、邮箱、多个电话号码,你能够像下面这样去解析这个记录:
注意,不论电话号码多长(即便是0个),解析出来的始终是一个列表list。*变量也能够解析列表开始位置的变量。例如一个公司有一份8个季度的价格报表,想统计前7个季度的价格平均值,你能够像下面这么作:
讨论:扩展迭代解析是为了更好的解析不定长度的迭代器。迭代器的结构常常是已知的(好比最后一个元素是电话号码),*解析使开发更容易解析出迭代器中的元素。
*语法可以更好用的解析可变序列。好比,下面的由不一样元祖组成的序列:
*解析也能够结合字符串处理操做进行解析,例如字符串的split分割:
有时你想抛弃一些元素,你可使用一个*变量去代替一些要抛弃的变量名,你可使用一个通用的抛弃变量名(好比*_,或者*ign),例如:
*解析能够帮助列表进行更多的功能处理。例如,你能够简单分开列表的头部和尾部部分:
利用这个能够写出更好的递归算法,好比:
(解释一下这句话head+sum(tail) if tail else head:意思是若是存在tail就返回head+sum(tail)不然返回head,这样就知足了当递归到最后一个数时,实际*tail是一个空,则只返回了head,达到了递归求和的效果)
由于递归的限制,递归没有展现Python强大的特性,最后一个例子只是实践了一下学术的好奇心。
 
1.3保留最后N个元素
问题:想要保留迭代器或其余处理的最后几项元素历史记录
解决方法:能够用集合队列来保存历史。例如,下面的代码完成了一个简单的文本匹配队列中的行,当在队列的前N行中匹配到文本时经过yield记录
讨论:搜索的代码。一般使用涉及yield的迭代方法,就像上面的例子,将使用搜索的结果和搜索过程分离开。若是想知道更多关于迭代器的能够查看本书的4.3(4章节)
用deque(maxlen=N)建立一个固定大小的队列,当队列满了时进来新数据,会将最先进来的数据移除出去(先进先出)。例如:
虽然也可使用list进行这样的操做(好比增长或删除),可是使用队列deque会更优雅,且速度更快
在须要任何一种简单的队列结构时均可以使用deque,若是不给deque一个最大的限制长度,你能够得到一个无限大小的队列,能够对队列进行append和pop操做,例如:
在deque中增长或删除元素的复杂度是O(1),可是在list中插入或移除元素的复杂度是O(N)
 
1.4查找最大或最小的N个元素
问题:想要将集合中最大的或最小的N个元素放到一个list中
解决方法:heapq模块有两个方法,nlargest()和nsmallest()。这两个方法能够实现咱们想要的功能。例如:
这两个方法容许经过一个叫key的参数来使他们能够去处理更加复杂的数据结构,例如:
讨论:若是要查找N个最大或最小值时,N超过了集合的数量,这个模块提供了一个更优秀的方法。先将数据转换到一个list中,而后会像一个heap堆同样对它进行排序,例如:
这个heap最重要的特色是heap[0]始终是最小的一个元素,此外你能够经过heapq.heappop()更容易的找到元素,经过它能够将heap底部的元素删除(最小的值),这个操做的复杂度是O(log N),N为heap的长度。例如,找到最小的三个元素,你能够这样写:
nlargest()和nsmallest()方法更适合去寻找数据中相对最小的值,若是你只想找一个最小的或最大的值(N=1),这两个方法比min()和max()更快。可是,若是N和集合自己的长度相同(对整个集合进行排序),这两个方法没有sorted(items)或者使用切片sorted(:N)快。
必须根据实际状况来肯定使用最优的方法(好比N和输入的数据长度接近时,使用sort排序)
虽然它不是必须使用的菜谱,heap的实现仍是值得去学习的,heap常常出如今正规的算法和数据结构书上,这篇关于heapq模块的文章只是讨论了一下底层的实现信息。
 
1.5实现一个优先级队列
问题:想要实现一个根据给定的优先级去排序的队列,而后每次pop都返回最高优先级的数据
解决方法:下面的类经过heapq实现了一个简单的优先级队列
说明:若是优先级priority用正数是pop()出来的是最小的数,用负数pop()出来的是最大的数,因此这里写做-priority
接下来是一个使用该类的例子:
能够看到pop()出来的是优先级最高的,一样能够看到相同优先级的,pop()先出来先插入到队列的数
讨论:这段代码更关心的是heapq模块,heapq.heappush()和heapq.heappop()这两个方法,插入和移除队列数据,第一个元素是最低优先级的(能够参考本书第1.4节)。heappop()老是返回优先级最低的数,push和pop操做的空间复杂度是O(log N),N是heap的长度,因此即便heap数据不少效果也会很好。
在本书中,队列由元祖(-priority,index,item)组成,优先级取的最高优先级,这和默认的排序是相反的。
index变量是为了标识相同优先级的项,index是自增的,相同优先级的项排序与他们插入的顺序有关,index在比较相同优先级项的操做过程当中扮演了一个重要的角色。
举一个不能排序的例子:
若是使用(priority,item)格式的元祖,他能够比较不一样优先级的项,可是不能比较相同优先级的项,例如:
经过增长index组成的(priority,index,item)元祖,能够解决相同优先级的问题(Python历来不用困恼比较的问题)。
若是想将队列用在线程通讯,须要增长优先级去锁定或发信号(详见本书12.3章节),这篇文章更深层次的介绍了heapq模块的原理和heaps的实现
 
1.6字典中实现key映射一个复杂的values值
问题:使用字典,一个key想对应多个value(也叫做multidict)
解决方法:字典是一个key对一个value的映射。若是想要key映射多个value,能够将多个value存储到list或者set容器中。例如,下面的例子。
根据用途去选择存储到列表list和集合set中。若是你想要记住排序就用list,若是想要去重(且不在意排序)就用集合set
经过collections模块的defaultdict方法更容易的构建字典结构,defaultdict自动初始化value的类型,你只要添加元素就能够了。例如:
注意defaultdict直接建立一个字典的key的value类型(及时它如今没有在字典中建立)。若是你不想要这个特性,你能够用字典的setdefault()方法来替换。好比下面的例子。
而后,不少程序发现setdefault()有一个很差的地方——每次使用以前都须要初始化一下实例(例子中的list[])
讨论:理论上构造一个字典是简单的,而后,让字典本身去初始化是比较麻烦的。例如,你可能像下面这样去写代码:
使用defaultdict可使得代码更加简洁:
本书极力去解决数据处理中组织记录的问题,例如本书1.15章节的例子
 
1.7保留字典的顺序
问题:建立一个字典,而且想要控制字典的顺序,以方便迭代和序列化
解决方法:使用collections模块中的OrderedDict方法能够控制字典中元素的排序,该方法准确的记录了数据插入的顺序,例如:
建立一个字典,OrderedDict后续能够很方便的对数据进行序列化或从新定义数据的格式操做。例如,想要严格控制JSON格式的域的出现顺序,首先使用OrderedDict存储数据能够实现它。
讨论:OrderedDict内部原理是使用一个双向链表保持插入顺序。插入新元素时会在尾部插入,再插入已存在key的数据时不会改变排序。
注意OrderedDict的长度是普通字典的两倍多,由于额外的建立了一个链表。因此你须要根据你的应用程序的需求判断是否要增长额外的内存消耗来使用OrderedDict。
 
1.8字典的计算
问题:对字典数据进行多样的计算(好比求最小值、最大值、排序等)
解决方法:建立一个价格字典,将股票名称映射到价格
为了更好的计算字典的数据,比较好用的方法是使用zip()去转换字典的key和value。下面的例子是将怎么找到最小和最大价格以及对应的股票名称
同理,使用zip()和sorted()来进行字典的排序,像下面这样:
再作这种计算式,要注意zip()建立的 迭代只能使用一次,例以下面的错误事例:
讨论:若是尝试在字典里执行普通的数据减小,你会发现程序只会打印出key,value没有被打印出来,好比下面的例子
而后大多数时候咱们须要的某一信息对应的key(好比哪一个股票的有最低的价格?)
咱们能够经过应用一个去的key的函数来得到最小或最大值的key,例如:
zip()解决了字典成对插入数据到列表的问题。当比较元祖时,value元素将先被比较,而后才是key进行比较。为咱们作排序给出了极大的方便。
必须注意相同value的排序,他会根据key的排序给出排序,好比下面的例子:
说明一下zip(),zip()会对列表进行压缩成一个列表,每一项都是一个元祖,他会将每一个列表的list[i]位置的组合成一个元祖,并放在i位置,注意zip()组成的列表长度是压缩的列表的最短的一个的长度。
 
1.9查找两个字典中的共同点
问题:有两个字典,想查找两个字典中相同的地方(key或者value)
解决方法:有两个字典:
去查找两个字典中相同的地方,经过使用keys()和items()两个方法的set集合操做来实现,例如:
这种操做能够实现改变或过滤字典内容。好比,想要将选中的key移到一个新的字典中,下面是一个简单的例子:
讨论:字典是一系列的key和value的映射,keys()方法返回了字典的所有的key(keys-view对象)。它支持集合操做,好比并集交集差集。这样当你想对字典进行集合操做时,能够直接使用keys-view对象。而不用先将他们转换成set集合了。
items()返回的是字典所有的(key,value)对(keys-view对象)。这个对象也支持简单的set集合操做。
虽然values()和这相似,可是values()不支持set集合操做。某种程度上。items()包含的values不具备keys()的惟一性,使得作集合操做时可能会产生一些问题。若是必定要这么作运算,建议先转换成set集合再进行操做
 
1.10从序列中移除重复数据且保持序列的顺序
问题:想要移除序列中重复的值,又想保持序列元素的顺序
解决方法:若是序列中的值是hashable的(hashable:能够当作字典中的key),这个问题能够用set和迭代很容易的解决,好比:
下面是一个使用这个方法的例子:
这个只有序列值是hashable时能够用,若是要去重unhashable类型的(例如字典),你能够像下面这样作一点改变:
这里指定key参数是为了将序列的元素转成成哈希结构的重复数据,
解释一下这个方法:val=item if key is None else key(item) 若是key是None则val=item,若是key不为None则val=key(item);yield只能在函数中用,能够用作迭代,保存了数据的存储顺序
下面展现它是怎么工做的:
看看实例是怎么用的:key=lambda d:(d['x'],d['y'])实际是取出序列中每一个字典的x和y的值组成一个元祖,既然key确定不为None,则val=key(item),即取出每一个子字典中的x和y的值组成元祖,而后插入到set属性的seen集合中,seen始终去重。经过yield item保存了迭代结果(既包括值,又包括顺序)
复杂结构的数据去重处理,第二种方法的效果更好。
讨论:若是只是想去重不在乎排序,能够用set直接实现。例如:
这种方法不会保持排序,所得结果顺序杂乱。上面的方法能够解决这个问题。上面的方法不只可以处理列表,也能够处理其余的方法,好比去重读取的文件行:
方法中的key模仿了内建函数sorted()、min()和max(),例如本书的1.8和1.13章节
 
1.11命名切片
问题:程序代码是不值得读的硬编码切片,想要清除它
解决方法:将记录中特殊的数据使用固定的格式提取出来(例如从一个f文件或者相似的格式)
替换上面的作法,为何不这么命名切片呢?
在第二个版本中,咱们避免了使用硬编码,使的咱们想作什么变得更加清晰。
讨论:通常来讲,大量的硬编码会影响易读性,假如一年后你再读这些代码,你会想当初写这段代码的时候你在想什么?而咱们的解决方法可使得代码更加清晰。
内建函数slice()能够建立一个切片对象,用在任何容许使用切片的地方,好比:
若是有一个切片实例s,你能够同过s.start、s.stop、s.step属性得到更多的信息,好比:
start为切片起始位置,stop为切片终止位置,step为切片的步长
另外,经过indices(size)能够映射切片到一个特殊长度序列上,它将返回一个元祖(start,stop,step),这些值被合适的限定在界限范围内(能够避免索引时报IndexError异常),例如:
indices(size)解释:根据测试发现,他不会改变原有切片对象的step,若是start或者stop超出要适应的特殊序列s的长度,start会变成len(s),以保证切片索引不会超出size报异常,以下:
 
1.12将出现比较频繁的元素放到一个序列中
问题:有一个序列,想要将其中出现比较频繁的元素放到一个序列中。
解决方法:collections.Counter这个类就是为了解决这个问题,它和most_common()方法一块儿解决这个问题。
举个例子,要找出一个序列中出现次数最多的词,咱们能够这么解决:
讨论:根据输入的元素,Counter对象能够处理输入元素中全部的hashable元素,一个Counter是元素映射它出现次数的字典,例如:
若是想要手动实现数量自增(累加其余序列中的关键词出现的次数),能够用下面的方法:
也可使用Counter的update()方法来实现上面的功能:
Counter实例有一个不为人知的特性,能够结合数学运算,例如:
Counter对象是一个统计数据很是有利的一个工具,你应该更喜欢经过字典去手动解决问题。
 
1.13经过字典的某一个key对字典的列表进行排序
问题:根据一个或多个字典域的值来对字典列表进行排序
解决方法:这种类型的问题,能够经过operator模块的itemgetter方法来解决。假设从数据库中查询出数据用到网站上,返回的数据格式以下:
经过字典中任何一个域进行排序都是很是容易的,例如:
(说明:itemgetter反回了对应key的value,若是是数字则是返回对应位置的值;sorted(data,key)是指date数据按照key关键词进行排序)
itemgetter()方法也能够接受多个关键词,好比下面的代码:
itemgetter能够这么分开写:
a=itemgetter('lname')
for i in rows:
     print(a(i))
讨论:在这个例子中,rows传递给内建函数sorted(),sorted()接收了参数key,key参数是为了传递给rows做为输入,而后回调rows按照key的值进行排序的结果。itemgetter()函数就是建立了这样的一个key参数。
operation.itemgetter()函数得到rows中的一个指望值。它能够是一个字典的域名,一个list元素的数值,或者任何一个能够经过对象的__getitem__()方法得到的值。若是给itemgetter()传递多个参数,会将返回的多个元素放到一个元祖中,而且sorted()将根据这个元祖进行排序。这对于经过多个域同时进行排序是很是有用的(好比名或行,就像上面的例子同样)
itemgetter()有时用lambda表达式进行代替,好比:
这个解决方案也很好,可是使用itemgetter()会更快。因此你能够处于性能的考虑来选择。
最后,但也很重要的,能够将itemgetter()应用到本书讲过的min()和max(),例如:
 
1.14对不支持比较操做的类进行排序
问题:想要对类进行排序,可是类自己不支持排序操做
解决方法:使用内建函数sorted()中的key参数指定对象中的一个可调用的关键词,而后按照key进行排序。例如,程序中有一个User序列实例,而后想按照User的属性user_id进行排序,这样咱们须要提供一个User实例做为输入,而后返回user_id,例如:
能够用operator.attrgetter()来替换lambda
讨论:根据我的喜爱来选择使用lambda仍是attrgetter()。可是attrgetter()会稍微快一点,而且支持同时提取多个域,它和本书1.13节中讲的operator.itemgetter()相似。例如,若是User实例有first_name和last_name属性,你能够像下面这样进行排序:
一样的,咱们也能够在min()和max()中使用这个方法,例如:
 
1.15按照某一个域进行分组
问题:有一个字典或者实例的序列,想要按照某个域的值进行分组去迭代数据,好比日期。
解决方法:itertools.groupby()函数能够用来对数据进行分组,好比下面的字典列表的数据。
如今支持经过date分组而后进行迭代,首先须要经过域来进行排序(这个例子的域是date),而后使用itertools.groupby():
讨论:groupby()函数经过遍历序列和寻找定义的值(或者经过给定的key方法返回的值)来工做。每次循环都返回这个值以及按照这个值进行的分组(值相同的为一组)。
一个重要的步骤是先要将数据按照这个域进行排序,由于groupby()只能处理连续的数据,若是不先进行排序将不能正确完成分组。
若是只是想对数据进行简单分组而不在乎顺序,使用defaultdice()建立一个multidict,就像本书1.6章节总描述的,例如:
每一个date下的记录能够这样用:
像后面这个例子,不须要先对数据进行排序。若是不考虑内存,第二种方法比第一种先排序在用groupby()来进行分组的速度快。
 
1.17提取字典的子集
问题:想要提取一个字典的子集存储到另外一个字典中
解决方法:经过字典推导能够很轻松的完成,例如:
讨论:字典推导能够实现的也能够经过建立一个元祖序列,而后传递给dict()建立字典来实现,例如:
可是字典推导的方法更快更简洁(是使用dict()两倍快)
有多重方法能够实现一样的事情,好比第二个例子能够重写成下面的样子:
可是研究代表,这种方式比第一种方式慢1.6倍。性能问题须要花更多的时间去学习。本书14.13介绍了时间和性能。
 
1.18映射名称到序列元素
问题:咱们经过位置读取列表或元祖的顺序,可是有时候难以读取,而后想要数据的结构对位置的依赖小一点,经过名称来得到元素
解决方法:collections.namedtuple()提供了这个方法。经过使用元祖对象的object.collections.namedtuple()工厂方法返回了一个标准Python元祖类型的子类,这种方法花销更小。提供一个类型名称和须要的域,而后他会返回一个能够实例化的类,将值放入定义的域中。例如:
虽然一个namedtuple的实例看起来和class实例很像,可是它支持全部元祖的操做,好比索引和解析,例如:
元祖命名的一个主要用法是将元素的位置和操做进行解隅。这样若是你从数据库中得到一个大的元祖列表,而后经过访问元素的位置来进行操做,若是你在表中增长一个新字段你的代码就不能用了,如今若是返回一个元祖和元祖的名称就能够解决这种方法。
举个使用元祖排序进行操做的例子:
参照元素位置使得代码不容易理解而且依赖记录的结构。接下来是使用namedtuple的版本 :
若是例子中的records列表已经包含这样的实例,就能够不用经过namedtuple对Stock进行转换了
讨论:namedtuple能够当作字典的替换使用,且存储空间比字典小。若是想要构建涉及到字典的大数据结构,使用namedtuple将更有效。可是药注意,与字典不一样的是namedtuple是不可变的。好比:
若是想要改变某一个属性,须要经过_replace()方法来更改namedtuple实例的属性,它建立了一个新的namedtuple,值被替换了。例如:
_replace()能够很方便的去填充namedtuple的可选域,这样能够建立一个包含默认值的元祖模型,而后经过_replace()来建立一个新的实例而且替换值。例如:
 
(方法中使用*s表示全部参数存在元祖中;**s表示全部参数存在字典中)
下面演示一下怎么使用上面的代码:
若是构建的数据结构有不少个属性须要变化,不建议使用namedtuple,能够考虑用__slots__(本书8.4章)
 
1.19同时转换并计算数据
问题:须要执行汇集函数(好比sum(),min(),max()),可是首先须要筛选和转换数据
解决方法:一个结合数据转换和计算的很是优雅的方法,使用生成器表达式传递参数。例如,若是想要计算平方和,能够像下面这样:
下面是另外一个例子:
讨论:这个解决方案展现了将生成器表达式 做为函数的单个参数的精妙之处(好比你不须要重复操做)。下面的声明效果相同:
 
使用生成器做为参数比建立一个临时的列表更有效、更优雅。好比,若是不用生成器表达式 ,你可能像下面这样作:
在这个例子中使用了额外的步骤建立了一个额外的列表。若是是小列表影响不大,可是若是列表比较大则会建立一个大的临时数据结构,用过一次后就被抛弃。使用生成器转换数据存储效率更高。
(生成器表达式用(),返回的是一个迭代;列表解析用的是[],返回的是一个list。)
在使用汇集函数好比min()、max()的key参数时,使用迭代更好一些。好比portfolio的例子,能够这样考虑:
 
1.20合并多个字典到一个单一的映射
问题:有多个字典或映射,想要逻辑结合到一块儿变成一个映射去执行某些操做。好比查找值或者确认某些键是否存在
解决方案:假若有两个字典
假如你想在两个字典中执行查找操做(好比先在a中查找,而后a中不存在再从b中进行查找),一个简单的方法是使用collections中的ChainMap方法。例如:
讨论:ChainMap组合多个字典而且在逻辑上变成一个,可是字典没有被真正的合并到一块儿。ChainMap只是建立了一个字典的列表,而且重定义了列表中字典操做去扫描列表。字典大多数操做均可以使用,好比:
若是字典中有重复的key键,对应的值将从第一个能获得key的字典中读取。好比这个例子中的c['z'],将会从a字典中得到值而不是从b中得到。
改变列表中的字典元素将会影响到列表中的第一个字典(只影响到第1个字典,不会影响到其余字典),好比:
ChainMap和编程语言中的做用域变量一块儿用时颇有用(好比globals,locals等)。实际上有方法可使得它更简单:
(经过ChainMap实例的new_child()在列表中头部增长一个新的列表子项;经过parents是得到了除了当前列表元素的其余元素,至关于[1:]。)
做为ChainMap的替换选择,可能更想要的经过update()将字典合并到一块儿,例如:
这个方法须要创一个新的字典来区分原来的字典对象(或者破坏原来的字典)。并且若是原来的字典有改变,合并后的字典不会有变化。好比:
ChainMap是用的原始的字典,因此不会有这个特性(即原始字典数据更改了,合并后的列表字典的数据也会变),好比:
相关文章
相关标签/搜索