collection在英文当中有容器的意思,所以顾名思义,这是一个容器的集合。这个库当中的容器很多,有一些不是很常用,本篇文章选择了其中最常用的几个,一起介绍给大家。
defaultdict
defaultdict
可以说是这个库当中使用最简单的一个,并且它的定义也很简单,我们从名称基本上就能看得出来。它本质上就是一个dict
,它是dict
的优化版,能够解决的是我们使用dict
当中最常见的问题,就是key为空的情况。
使用dict
在正常情况下,我们在dict
中获取元素的时候,都需要考虑key为空的情况。如果不考虑这点,那么当我们获取了一个不存在的key,会导致系统抛出异常。我们当然可以在每次get之前写一个if判断,但是这很麻烦,比如:
if key in dict:
return dict[key]
else:
return None
当然,这是最笨的方法,dict
当中为我们提供了带默认值的get方法。比如,我们可以写成:
return dict.get(key, None)
这样,当key不在dict
当中存在的时候,会自动返回我们设置的默认值。这个省去了很多麻烦的判断,但是在一些特殊情况下仍然存在一点问题。举个例子,比如当key存在重复,我们希望将key相同的value存进一个list当中,而不是只保留一个。这种情况下写成代码就会比较复杂:
data = [(1, 3), (2, 1), (1, 4), (2, 5), (3, 7)]
d = {}
for k, v in data:
if k in d:
d[k].append(v)
else:
d[k] = [v]
由于dict
的value是一个list,所以我们还是需要判断是否为空,不能直接使用默认值,间接操作当然可以,但是还是不够简单:
for k, v in data:
cur = d.get(k, [])
cur.append(v)
d[k] = c
这和使用if区别并不大,为了完美解决这个问题,我们可以使用collections当中的**defaultdict
**
defaultdict用法:
使用defaultdict
之后,如果key不存在,容器会自动返回我们预先设置的默认值。
其他的使用和dict
一摸一样,它也具有defaultdict.keys()
,defaultdict.values()
以及defaultdict.items()
默认值为list
默认值为list是defaultdict
用的最多的场景
其基本用法为:
from collections import defaultdict
d = defaultdict(list)
for k, v in data:
d[k].append(v)
例子:
from collections import defaultdict
d = defaultdict(list)
s = [('yellow',1),('blue',2),('yellow',3),('blue',4),('red',5)]
for k, v in s:
d[k].append(v)
print(d)
# defaultdict(<class 'list'>, {'yellow': [1, 3], 'blue': [2, 4], 'red': [5]})
print(d.keys())
# dict_keys(['yellow', 'blue', 'red'])
print(d.values())
# dict_values([[1, 3], [2, 4], [5]])
print(d.items())
# dict_items([('yellow', [1, 3]), ('blue', [2, 4]), ('red', [5])])
上述例子中,defaultdict(list)
代表如果key不存在,容器会自动生成我们预先设置的默认值,所以当一个新的key出现时,它的value自动就被创建成了一个空list [],所以其用法十分简洁。不用再去用其他的判断了。
默认值为int
当然默认值也可以是int
defaultdict(int)
如果key不存在,自动生成0
s = "mississippi"
d = defaultdict(int)
for ch in s:
d[ch]+=1
print(d)
# defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})
默认值为set
当然默认值也可以是set,目的是为了去重
defaultdict(set)
如果key不存在,自动生成空集合set()
s = [('red', 1), ('blue', 2), ('red', 3), ('blue', 4), ('red', 1), ('blue',
4)]
d = defaultdict(set)
for k,v in s:
d[k].add(v)
print(d)
# defaultdict(<class 'set'>, {'red': {1, 3}, 'blue': {2, 4}})
自定义默认值
需要注意的是defaultdict
传入的默认值可以是一个数据类型也可以是一个自定义函数。如果我们传入int
,那么默认值会被设置成int()
的结果,也就是0,如果我们想要自定义或者修改,我们可以传入一个自定义函数,比如:
# 将自定义默认值为3
d = defaultdict(lambda : 3)
data = [(1, 3), (2, 1), (1, 4), (2, 5), (3, 7)]
for k, v in data:
d[k] += v
print(d)
# defaultdict(<function <lambda> at 0x000001BA65046280>, {1: 10, 2: 9, 3: 10})
# 解释:1 = 3(默认)+3 +4 =10, 2 = 3(默认)+1+5=9
与原生dic之间的转化
如果想把一个defaultdict
变为dict
,只需要简单的使用dict()
函数即可
但是要把dict
变为defaultdict
只能通过遍历的方式,注意不能直接defaultdict(dic)
(其中dic是一个已有的字典),因为defaultdict
的参数是传入的默认值,它只可以是一个数据类型也可以或一个自定义函数,并不能是一个已有的字典
如:
dic = {'m': 1, 'i': 4, 's': 4, 'p': 2}
d = defaultdict(int)
# `dict`变为`defaultdict`只能通过遍历的方式
for k, v in dic.items():
d[k] += v
print(d)
# defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})
# 使用`dict()`函数直接可以把`defaultdict`变为`dict`
print(dict(d) == dic)
# True
Counter
这是一个非常常用和非常强大的工具,我们经常用到。顾名思义,Counter
就是个计数器的数据结构
在我们实际的编程当中,我们经常遇到一个问题,就是计数和排序。特别是在nlp任务中使用Counter要比自己手动用dict实现简洁的多,比如说我们在分析文本的时候,会得到一堆单词。其中可能有大量的长尾词,在整个文本当中可能只出现过寥寥几次。于是我们希望计算一下这些单词出现过的数量,只保留出现次数最高的若干个。
这个需求让我们自己实现当然也不困难,我们完全可以创建一个dict
,然后对这些单词一个一个遍历。原本我们还需要考虑单词之前没有出现过的情况,如果我们上面说的defaultdict
,又要简单许多。但是我们还是少不了计数然后排序的步骤,如果使用Counter这个步骤会缩减成一行代码。
一个 Counter
是一个dict
的子类,用于计数可哈希对象。它是一个集合,元素像字典键(key)一样存储,它们的计数存储为值。计数可以是任何整数值,包括0和负数。
Counter
对象和其他的使用和defaultdict
一样,它也继承了字典的所有方法,如Counter.keys()
,Counter.values()
以及Counter.items()
。只不过它重写了dict中的一些函数并添加了一些新的函数
从现有数据结构转换到Counter
元素从一个 iterable
被计数或从已有的 counter
或mapping
初始化。
如果是iterable
是字符串、列表、集合(不过对集合计数的意义不大,因为肯定只有一个)则会直接对其计数并返回计数结果。
如果是从dict或已有的counter转化那么只会转化为counter类型。
mapping
指的是从一个类似于**kwargs
的方式初始化counter
返回的显示顺序默认是按照value的从大到小顺序,不过counter本质是一个dict,其实际上是无序的,所以只能通过key进行访问
# 创建一个空counter
c = Counter()
# 从字符串转化为counter,会对每个ch进行计数并返回counter
c = Counter('gallahad')
# Counter({'a': 3, 'l': 2, 'g': 1, 'h': 1, 'd': 1})
# 从列表转化为counter,会对每个元素进行计数并返回counter
c = Counter(['apple', 'apple', 'pear', 'watermelon', 'pear', 'peach'])
# Counter({'apple': 2, 'pear': 2, 'watermelon': 1, 'peach': 1})
# 从集合转化为counter,会对每个元素进行计数并返回counter
c = Counter({1,2,3,4,3,4})
# Counter({1: 1, 2: 1, 3: 1, 4: 1}),意义不大,因为集合计数每个元素数量一定为1
# 从字典转化为counter
c = Counter({'red': 4, 'blue': 2}) # a new counter from a mapping
# Counter({'red': 4, 'blue': 2})
# 从`mapping`类似于`**kwargs`的方式初始化counter
c = Counter(cats=4, dogs=8)
# Counter({'dogs': 8, 'cats': 4})
# 从已有Counter转化
c = Counter(Counter({'red': 4, 'blue': 2}))
# Counter({'red': 4, 'blue': 2})
与dict的不同处
Counter与字典还有个不同的地方,它有一个字典接口,如果引用的键没有任何记录,就返回一个0,而不是弹出一个 KeyError
:
c = Counter(['eggs', 'ham'])
# 对于不存在的key返回0而不是报错
c['bacon']
# 0
设置一个计数为0不会从计数器中移去一个元素。使用 del
来删除它:
>>> c['sausage'] = 0 # counter entry with a zero count
>>> del c['sausage'] # del actually removes the entry
12
在 3.7 版更改: 作为dict
的子类,Counter
继承了记住插入顺序的功能。 Counter 对象进行数学运算时同样会保持顺序。 结果会先按每个元素在运算符左边的出现时间排序,然后再按其在运算符右边的出现时间排序。
Counter对象除了继承了字典的方法以外,还提供了四个其他函数:
elements()
,most_common(n)
,subtract()
,update()
以及+
,-
等运算符
常用函数以及运算符
elements()
返回一个迭代器,我们需要用list()
进行打开。
其中每个元素将重复出现计数值所指定次。 元素会按首次出现的顺序返回。配合使用sorted()可以返回有序的列表。
如果一个元素的计数值小于1,elements()
将会忽略它。
c = Counter( b=2, a=4, c=0, d=-2)
c.elements()
# <itertools.chain object at 0x0000023160DF4670>本身返回迭代器,需要用list()打开
list(c.elements())
# ['b', 'b', 'a', 'a', 'a', 'a'] 按首次出现的顺序返回
sorted(c.elements())
# ['a', 'a', 'a', 'a', 'b', 'b'] 返回有序的
most_common(n)
返回一个列表,其中包含 n 个最常见的元素及出现次数的元组 即 (element,count)
对。
按常见程度由高到低排序。
如果 n 被省略或为 None
,most_common()
将返回计数器中的所有元素。
计数值相等的元素按首次出现的顺序排序
Counter('abracadabra').most_common(3)
# [('a', 5), ('b', 2), ('r', 2)]
Counter('abracadabra').most_common()
# 如果 n 被省略或为 `None`,`most_common()`将返回计数器中所有元素。
# [('a', 5), ('b', 2), ('r', 2), ('c', 1), ('d', 1)]
update()
update()
可以将两个Counter相加,它会自动将两个Counter合并,相同的key对应的value累加。
从 迭代对象 计数元素或者 从另一个 映射对象 (或计数器) 添加。 但是字典的用法 dict.update()
是替换,而不是加上。另外,迭代对象 应该是序列元素,而不是一个 (key, value)
对。
同样:输入和输出都可以是0或者负数。
c = Counter(a=3, b=1)
d = Counter(a=-4, b=2)
c.update(d)
c
# Counter({'b': 3, 'a': -1})
subtract()
update()
可以将两个Counter相减,它会自动将两个Counter合并,相同的key对应的value相减。
从 迭代对象 或 映射对象 减去元素。像 dict.update()
但是是减去,而不是替换。
输入和输出都可以是0或者负数。
c = Counter(a=4, b=2, c=0, d=-2)
d = Counter(a=1, b=2, c=3, d=4)
c.subtract(d)
c
# Counter({'a': 3, 'b': 0, 'c': -3, 'd': -6})
数学符号运算
Counter对象还提供了几个数学操作,+
和-
,通过加上或者减去元素的相应计数。
&
,|
,交集和并集返回相应计数的最小或最大值。
注意:符号操作都可以接受带符号的计数的输入,但是不同于前面的函数subtract()和update(),其输出会忽略掉结果为零或者小于零的计数。
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)
## 通过符号运算的结果只保留正数!!!##
# 相加 c[x] + d[x] 对应subtract函数,结果只保留正数
c + d
# Counter({'a': 4, 'b': 3})
# 相减 c[x] - d[x] 对应subtract函数,结果只保留正数
c - d
# Counter({'a': 2})
# b:-1是负数输出会直接忽略
# intersection交集: min(c[x], d[x]) ,结果只保留正数
c & d
# Counter({'a': 1, 'b': 1})
# union并集: max(c[x], d[x]),结果只保留正数
c | d
# Counter({'a': 3, 'b': 2})
单目符号操作符意思是取计数器的所有正数(‘+Counter’)或倒数(‘-Counter’)并去除0以及负数。
c = Counter(a=2, b=-4,c=0)
+c
# Counter({'a': 2}) 可以简单理解为只返回Counter中的正数
-c
#Counter({'b': 4}) 可以简单理解为只返回Counter中的负数,并取绝对值
其他常用案例
sum(c.values()) # total of all counts
c.clear() # reset all counts
list(c) # list unique elements
set(c) # convert to a set
dict(c) # convert to a regular dictionary
c.items() # convert to a list of (elem, cnt) pairs
Counter(dict(list_of_pairs)) # convert from a list of (elem, cnt) pairs
c.most_common()[:-n-1:-1] # n least common elements 倒序
+c # remove zero and negative counts
123456789
deque
我们都知道queue是队列,deque也是队列,不过稍稍特殊一些,是双端队列。对于queue来说,只允许在队尾插入元素,在队首弹出元素。而deque既然称为双端队列,那么说明它的队首和队尾都支持元素的插入和弹出。相比于普通的队列,要更加灵活一些。
除了常用的clear、copy、count、extend等api之外,deque当中最常用也是最核心的api还有append、pop、appendleft和popleft。从名字上我们就看得出来,append和pop和list的append和pop一样,而appendleft和popleft则是在队列左侧,也就是头部进行pop和append的操作。非常容易理解。
在日常的使用当中,真正用到双端队列的算法其实不太多。大多数情况下我们使用deque主要有两个原因,第一个原因是deque收到GIL的管理,它是线程安全的。而list则没有GIL锁,因此不是线程安全的。也就是说在并发场景下,list可能会导致一致性问题,而deque不会。另一个原因是deque支持固定长度,当长度满了之后,当我们继续append时,它会自动弹出最早插入的数据。
比如说当我们拥有海量的数据,我们不知道它的数量,但是想要保留最后出现的指定数量的数据的时候,就可以使用deque。
from collections import deque
dque = deque(maxlen=10)
# 假设我们想要从文件当中获取最后10条数据
for i in f.read():
dque.append(i)
namedtuple
namedtuple很特殊,它涉及到元编程的概念。简单介绍一下元编程的概念,我们不做过多的深入。简而言之,就是在常见的面向对象当中。我们都是定义类,然后通过类的构造函数来创建实例。而元编程指的是我们定义元类,根据元类创建出来的并不是一个实例,而是一个类。如果用模具和成品来分别比喻类和实例的话,元类相当于是模具的模具。
namedtuple是一个非常简单的元类,通过它我们可以非常方便地定义我们想要的类。
它的用法很简单,我们直接来看例子。比如如果我们想要定义一个学生类,这个类当中有name、score、age这三个字段,那么这个类会写成:
class Student:
def __init__(self, name=None, score=None, age=None):
self.name = name
self.score = score
self.age = age
这还只是粗略的写法,如果考虑规范,还需要定义property等注解,又需要很多代码。如果我们使用namedtuple可以简化这个工作,我们来看代码:
from collections import namedtuple
# 这个是类,columns也可以写成'name score age',即用空格分开
Student = namedtuple('Student', ['name', 'score', 'age'])
# 这个是实例
student = Student(name='xiaoming', score=99, age=10)
print(student.name)
通过使用namedtuple,我们只需要一行就定义了一个类,但是这样定义的类是没有缺失值的,但是namedtuple很强大,我们可以通过传入defaults参数来定义缺失值。
Student = namedtuple('Student', ['name', 'score', 'age'], defaults=(0, 0))
可以注意到,虽然我们定义了三个字段,但是我们只设置了两个缺失值。在这种情况下,namedtuple会自动将缺失值匹配上score和age两个字段。因为在Python的规范当中,必选参数一定在可选参数前面。所以nuamdtuple会自动右对齐。