首页 > Python笔记 阅读数:28

Python collectionsmo模块详解

通过前面的介绍,我们知道,Python 拥有一些内置的数据类型,如 Number(数值型)、Str(字符串型)、List(列表)、Tuple(元组)、Dict(字典)等, collections(容器)模块基于这些基础数据类型,“站在巨人的肩膀上”提供了几个有用的数据类型。

在 Python 官方文档中,collections 的定位是“高性能容量数据类型”(High-performance container datatypes),其主要数据类型包括但不限于如下五类:
下面分别给予简单介绍。

Python namedtuple

如前所述,元组可视为列表的常量版本,它是一种不变数据类型。创建元组时,在圆括号( )之内添加元素,并用逗号将不同元素隔开即可。例如,一个点的二维坐标可以如下表示。

In [1]: p = (3, 4)


但是,如果仅仅看到数值 (3, 4),由于缺乏可读性,我们很难看出这个元组表示的是一个二维空间的坐标点。当然,我们可以把这个坐标封装为一个可读性很好的类,但这又有点“大张旗鼓”,折腾劲太大,性价比不高。这时,namedtuple 就有用武之地了。顾名思义,namedtuple 就是“命名版的元组”。

创建命名元组的示例代码如下。
In [2]: from collections import namedtuple
In [3]: Point = namedtuple('Point', ['x', 'y'])
In [4]: p = Point(3, 4)
In [5]: p.x
Out[5]: 3
In [6]: p.y
Out[6]: 4

在 In [2] 处,我们先得从模块 collections 中将 namedtuple 导入,它可以返回一个新的元组子类,利用这个子类可以创建一个自定义的命名元组对象。

为了构造这样一个子类,namedtuple 的构造方法需要两个参数,分别是元组子类的名字( In [3] 处括号内的 Point )和其属性的名字。多个属性可以用列表的方括号括起来,不同属性用逗号隔开,如 In [3] 处的 ['x', 'y']。

In [4] 处这个命名给出了元组的实例 p。这时,Point 可理解为一个被 namedtuple 加工出来的简易版的类,然后可以用属性(通过“对象名.属性”的方式)而非索引来访问这个命名元组 Point 中的某个元素(见 In [5] 和 In [6] )。

由于加上了命名这个特性,代码更易于维护。它既保留元组元素的天然属性——不可变性,又具备由命名带来的可读性,二者结合,相得益彰,十分方便。

实际上,namedtuple 是一个工厂类。什么是工厂类呢?简单来说,通过它“加工”出来的依然是元组的子类,只不过不同的类“各有个性”罢了。这个过程有点类似于,由于加工参数不同,工厂使用相同的原材料可以生产出略有不同的零部件,但本质上,这些零部件属于同一个派系。

因此,从继承派系上来看,namedtuple 加工出来的个性化的类(如上述代码 In [3] 处生成的 Point )依然是元组的子类。我们可以通过如下代码验证创建的对象 p 是否为元组和 Point 的实例(instance)。
In [7]: isinstance(p, Point) #对象 p 是否为类 Point 的实例
Out[7]: True

在 In [4] 处,对象 p 是由类 Point 定义出来的,所以 p 自然是类 Point 的一个实例。这里用到了 Python 的一个内置函数 isinstance( )。该函数的功能是判断一个对象是否为一个已知类型的实例。
In [8]: isinstance(p, tuple)  #对象p是否为元组实例
Out[8]: True
从上面的输出可以看到,isinstance( ) 还可以做到“隔代指认”。严格来讲,Point 应该属于元组的子子类,但依然被认为是元组的一个实例。

既然元组和 namedtuple 有这么密切的关系,二者必然有很多相通的地方。事实也的确是这样,namedtuple 还有一个重要的优点,就是它与元组是完全兼容的。也就是说,我们依然可以用索引下标去访问一个 namedtuple 元素,示例如下。
In [9]: p[0]    #访问 p 中的第 0 个元素(下标从 0 开始计数)
Out[9]: 3 
In[10]: p[1] 
Out[10]: 4      #访问 p 中的第1个元素

甚至我们还可以像普通元组一样解包(unpacking)namedtuple 中的元素。解包是 Python 的特有属性,其表现形式为,把一个包含多个元素的对象(如列表、元组等)一次性地赋值给多个简单变量,对象内部的元素会被解开,并按照位置顺序,一一赋值给简单变量。
In [11]: a, b = p    #解包,用普通变量 m 和 b 接收 namedtuple 中的元素 x 和 y
In [12]: a, b         #输出验证
(3, 4)

Python deque

使用列表存储数据时,如果按索引访问元素,即执行只读操作,访问速度会很快。因为列表是线性存储的,因此列表元素的插入和删除操作(即写操作)就很慢。特别是当列表元素数据量很大时,插入和删除操作的效率简直低得令人难以容忍。

deque(双向队列)是为了实现高效插入和删除操作的数据类型,它特别适用于队列和栈的操作,示例代码如下。
In [1]: from collections import deque      #导入deque
In [2]: dq = deque(['a', 'b', 'c'])        #创建双向队列
In [3]: dq.append(1)                       #往队列右边添加一个元素 1
In [4]: print(dq)                          #输出验证
deque(['a', 'b', 'c', 1])
In [5]: dq.appendleft(2)                   #往队列左边添加一个元素 2
In [6]: print(dq)                          #输出验证
deque([2, 'a', 'b', 'c', 1])
In [7]: dq.insert(2, 'x')                  #在指定索引位置(2)插入元素 'x'
In [8]: print(dq)                          #输出验证
deque([2, 'a', 'x', 'b', 'c', 1])
In [9] : dq.pop()                          #弹出最右边的一个元素,并在队列中删除
In [10]: print(dq)                         #输出验证
deque([2, 'a', 'x', 'b', 'c'])
In [11]: dq.popleft()                      #获取最左边的一个元素,并在队列中删除
Out[11]: 2
In [12]: print (dq)                        #输出验证
deque(['a', 'x', 'b', ' c'])
In [13]: dq.remove('x')                    #删除指定元素 x
In [14]: print(dq)                         #输出验证
deque(['a', 'b', 'c'])
In [15]: dq.reverse()                      #队列逆序
In [16]: print(dq)                         #输出验证
deque(['c', 'b', 'a'])
上述代码仅仅演示了 deque 的部分增、删、改、查功能。有了这些好用辅助方法,我们就可以在双向列表中高效添加或删除元素了。

Python OrderedDict

我们知道,普通的字典是由一系列的键/值对构成的。在使用字典时,键是无序的。因此,在对字典对象做迭代时,由于无法确定关键字 key 的顺序,因此会带来操作上的不便。

如果想要保持关键字 key 的顺序,可以使用“定制版”的字典——有序字典(OrderedDict)。有序字典的底层是通过双向链表来实现的,内部通过 map( ) 函数对指定字典元素序列做映射,以高效存储“键/值对”,示例代码如下:
In [1]: from collections import OrderedDict
In [2]: od = OrderedDict () #创建有序字典
In [3]: od["a"] = 1         #添加字典元素
In [4]: od["c"] = 2
In [5]: od["b"] = 3
In [6]: print (od)          #验证输出
OrderedDict( [('a', 1), ('c', 2) , ('b' , 3)])

需要注意的是,OrderedDict 中的“键”是按照元素插入的顺序来排列的,而不按照键本身排序,我们可以用如下代码输出有序的键。
In [7]: list(od.keys())    #按照插入键的顺序返回
Out[7]: ['a', 'c', 'b']

OrderedDict 中有很多方法,下面我们仅挑选几个方法( update()、pop()、move_to_end())来说明用法。先介绍一下 update( ) 方法的作用,该方法用于向老字典 od 之中追加一个新字典,实际上,相当于合并了两个字典。
In [8]: keys=["apple", "banana", "cat”]
In [9]: value=[4, 5, 6]
In [10]: od.update(zip(keys,value))
In [11]: print(od)
OrderedDict ([('a', 1), ('c', 2), ('b', 3), ('apple', 4), ('banana', 5), ('cat', 6)])

下面说明 pop( ) 和 move_to_end( ) 方法的用法。
In [12]: od.pop('a')                #将键为 'a' 的字典元素弹出,并从有序字典中删除它
Out[12]: 1
In [13]: print(od)                  #输出验证
OrderedDict([('c', 2), ('b', 3), ('apple', 4), ('banana', 5), ( 'cat', 6)])
In [14]: od.move_ to _end('b')      #将键为 'b' 的元素移到队尾
In [15]: print(od)                  #输出验证  
OrderedDict([('c', 2), ('apple', 4), ('banana', 5), ('cat', 6), ('b', 3)])

Python defaultdict

使用字典时,如果所引用的键不存在,就会抛出异常—— KeyError,从而导致整个程序终止执行。如果希望键不存在时能返回一个默认值,就需要使用提供默认值的字典类型 defaultdict。
In [1]: from collections import defaultdict   #导入默认值字典
In [2]: dd = defaultdict(lambda: 'N/A')       #设置默认值
In [3]: dd['key1'] = 'abc' 
In [4]: dd['key1']                            #key1存在,正常输出
Out[4]: 'abc' 
In [5]: dd['key2']                            #key2不存在,返回默认值
Out[5]: 'N/A' 

需要注意的是,字典 defaultdict 的默认值是在某个键缺位时才会返回的值,这个“默认补位”的值需要在创建 defaultdict 对象时传入。在 In [2] 处,我们使用了一个匿名函数 lambda 来设置默认值。除了在键不存在时返回默认值,defaultdict 的其他行为与普通字典类型并无二样。

Python Counter

Counter 一词的中文含义就是“计算器”,在 Python 中它是 collections 包中提供的一个简易计数器类。例如,如果我们想统计某个单词出现的频率,一种简单的办法就是将单词作为字典的“键”,而将次数作为字典的“值”,然后用for循环轮询单词列表,每遇到同一个单词就让值 +1。其代码如【例 1 】所示。

【例 1 】利用字典统计词频(for-loop-count.py)
colors = ['red', 'blue', 'red', 'green', 'blue', 'yellow']
result = {}
for color in colors:
    if result.get(color)==None:   #如果是字典的新元素
        result[color] = 1         #添加新的键/值对
    else:
        result[color] += 1        #如果是字典的老元素,则值+1
print (result)
程序执行结果为:

{'red': 2, 'blue': 2, 'green':1, 'yellow': 1}


现在我们用 Counter 来实现与【例 1】相同的功能,参见【例 2 】。

【例 2】利用 Counter 统计词频(Counter-yellow.py)
from collections import Counter
colors = ['red', 'blue', 'red', 'green', 'blue', 'yellow']
result = Counter(colors)
print (diet(result))
程序执行结果为:

{'red': 2, 'blue': 2, 'green':1, 'yellow': 1}

【例 2 】的第 01 行导入了 Counter 类。第 02 行定义了一个列表 colors,第 03 行创建一个 Counter 对象 result,colors 是 Counter 计数的数据源。第04行输出统计结果。需要注意的是,Counter 并不能直接输出,必须显式地将 Counter 对象 result 转换为字典,才能正常输出。

显然,通过使用 Counter,代码更加简单了,也更加易读和易于维护了。

Counter 类中最常用的方法莫过于 most_common(n) 了,这里的 n 表示某个数字,它表示出现频率最高的几个对象,它以列表中内嵌元组的形式出现,每个元组对象由两部分构成,前者是对象,后者是对象出现的频率。

例如,如果我们想返回出现频率最高的两个单词,可以在【范 2 】中添加如下代码。

print(result.most_common(2))


于是,运行的结果中就多出了如下内容。

[('red', 2), ('blue', 2)]


上述结果表示,单词 red 出现了 2 次,单词 blue 出现了 2 次。如果我们想读取 blue 的数量,则可按照层次解析的方法应用如下语法。

result.most_common(2) [1][1]


上述语句看起来有点复杂,为什么会写成这番怪模样呢?但如果你对Python面向对象的语法比较熟悉,就不难理解。

首先,如前所述,result.most_common(2) 方法返回的是一个列表对象,其中包含两个元素(每个元素又是一个元组)。现在如果我们想提取第 1 个(从 0 开始计数)元素,那么就需要写成 result.most_common(2)[1] 这样的形式,这个操作返回的依然是一个元组对象,这个元组里依然有两个元素,一个是 'blue',一个是 2。现在我们想读取第 1 个(从 0 开始计数)元素,那么就得再添加一层方括号,自然就是 result.most_common(2) [1][1] 这样了。

相关文章