将学习变成乐趣,一直在路上
每天1990

导航

 
 
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是用的原始的字典,所以不会有这个特性(即原始字典数据更改了,合并后的列表字典的数据也会变),比如:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted on 2016-03-16 19:24  每天1990  阅读(897)  评论(0编辑  收藏  举报