Python作为一个“内置电池”的编程语言,标准库里面拥有非常多好用的模块,用好了能省去很多自造轮子。我们都知道,Python拥有一些内置的数据类型,比如str, int, list, tuple, dict等,而collections模块在这些内置数据类型的基础上,提供了几个额外的数据类型:
- namedtuple:生成可以使用名字来访问元素内容的tuple子类。
- defaultdict:带有默认值的字典。
- deque:双端队列,可以快速的从另外一侧追加和推出对象。
- Counter:计数器,主要用来计数。
- OrderedDict:有序字典。
- ChainMap:可把多个字典或者其它映射对象放在一起,组成一个单一的、可更新的映射对象。
Python 3新增加3个包装类:
- UserDict:字典的包装类。
- UserList:列表包装类。
- UserString:字符串包装类。
一、namedtuple
我们应该知道元祖是一个可迭代,不可变对象。tuple一旦初始化就不能修改(immutable)。
1 |
>>> classmates = ('Michael', 'Bob', 'Tracy') |
现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。
不可变的tuple有什么意义?因为tuple不可变,所以性能更优,多线程安全,不需要锁,不担心被恶意修改或者不小心修改。如果可能,能用tuple代替list就尽量用tuple。
namedtuple继承了tuple,主要用来产生可以使用名称来访问元素的数据对象,通常用来增强代码的可读性,在访问一些tuple类型的数据时尤其好用。怎么理解呢?类似于访问类实例属性一样来访问tuple的元素。我给个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 定义类; >>> class Websites: ... def __init__(self, name, url, founder): ... self.name = name ... self.url = url ... self.founder = founder # 类实例化; >>> Website = Websites(name='Sohu', url='http://www.google.com/', founder='张朝阳') # 显示实例属性; >>> print(Website.name, Website.url, Website.founder) Sohu http://www.google.com/ 张朝阳 |
举例:比如我们用户拥有一个这样的数据结构,每一个对象是拥有三个元素的tuple。使用namedtuple方法就可以方便的通过tuple来生成可读性更高也更好用的数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 |
from collections import namedtuple websites = [ ('Sohu', 'http://www.google.com/', u'张朝阳'), ('Sina', 'http://www.sina.com.cn/', u'王志东'), ('163', 'http://www.163.com/', u'丁磊') ] Website = namedtuple('Website', ['name', 'url', 'founder']) for website in websites: website = Website._make(website) #_make方法支持传入一个可迭代对象; print(website) |
解析结果如下:
1 2 3 |
Website(name='Sohu', url='http://www.google.com/', founder='张朝阳') Website(name='Sina', url='http://www.sina.com.cn/', founder='王志东') Website(name='163', url='http://www.163.com/', founder='丁磊') |
看这个结果很像什么,是不是就是类实例化这个动作。所以可以做如下操作:
1 |
Website = Website(name='Sohu', url='http://www.google.com/', founder='张朝阳') |
到这里可能会想,这么用有什么好处呢?直接写一个类不也是一样,使用nametuple可以帮助我们减少代码量,减少内存空间(自己定义类,内部做很多额外初始化操作)。另外,我们一般操作数据库或访问API时,返回的结果不是tuple就是dict类型。而使用nametuple就可以很快帮我们生成可以直接插入到数据库的数据。如下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# tuple; >>> User = namedtuple("User", ["name","age","height","address"]) >>> user_tuple = ("dkey", 23, 170, "shanghai") >>> user = User(*user_tuple) >>> user.name 'dkey' #or >>> user_tuple = ("dkey", 23, 170) >>> user = User(*user_tuple, 'shanghai') >>> user.address 'shanghai' # dict; >>> User = namedtuple("User", ["name","age","height","address"]) >>> user_dict = {"name":"dkey", "age":23, "height":170, "address":"shanghai"} >>> user = User(**user_dict) >>> user.name 'dkey' #or >>> user_dict = {"name":"dkey", "age":23, "height":170} >>> user = User(**user_dict, address="shanghai") >>> user.address 'shanghai' |
这里可以看到,我们使用了函数的“可变位置传参(*user_tuple)”和“可变关键字传参(**user_dict)”,函数传参支持直接传入一个tuple或者dict,其内部会进行解构成“位置参数”或“关键字参数”。
如果不想这么复杂,可以直接使用namedtuple的_make方法,这样一来就不需要使用这种形式“User(*user_tuple)”了,也自然不用关心“*”和“**”的区别。但是也有一个缺点就是必须属性和值一致,无法像我们上面那样可以随意填充。
1 2 3 4 5 |
>>> User = namedtuple("User", ["name","age","height","address"]) >>> user_tuple = ("dkey", 23, 170, "shanghai") >>> user = User._make(user_tuple) >>> user.name 'dkey' |
namedtuple除了_make方法外,还有一个常用的方法_asdict,可以用来帮我们生成OrderedDict类型。
1 2 3 |
>>> user = user._asdict() >>> user OrderedDict([('name', 'dkey'), ('age', 23), ('height', 170), ('address', 'shanghai')]) |
最后,namedtuple除了提供这些好用的特性外,同样也支持tuple原生特性,比如解包操作。
1 2 3 |
>>> name, age, *other = user >>> print(name, age, other) dkey 23 [170, 'shanghai'] |
namedtuple提供了一种新的数据结构,在实际中使用的很多,也非常好用。
二、defaultdict
1 |
class collections.defaultdict([default_factory[, ...]]) |
defaultdict是内置类dict的一个子类,返回一个新的类似字典的对象。它覆盖一个方法并添加一个可写的实例变量,其余的功能与dict类相同。
我们都知道,在使用Python原生的数据结构dict的时候,如果用d[key]这样的方式访问,当指定的key不存在时,是会抛出KeyError异常的。比如我们实现一个统计功能。该例子统计strings中某个单词出现的次数,并在counts字典中作记录。单词每出现一次,在counts相对应的键所存的值数字加1。但是事实上,运行这段代码会抛出KeyError异常,出现的时机是每个单词第一次统计的时候,因为Python的dict中不存在默认值的说法,可以在Python命令行中验证:
1 2 3 4 5 6 7 8 |
>>> user_dict = {} >>> users = ["aa","bb","cc","aa"] >>> for user in users: ... user_dict[user] += 1 ... Traceback (most recent call last): File "<stdin>", line 2, in <module> KeyError: 'aa' |
此时,我们不得不加一段判断代码:
1 2 3 4 5 6 7 |
user_dict = {} users = ["aa","bb","cc","aa"] for user in users: if user not in user_dict: user_dict[user] = 1 else: user_dict[user] += 1 |
看一下执行结果:
1 2 |
>>> print(user_dict) {'bb': 1, 'cc': 1, 'aa': 2} |
当然,除了使用判断外,dict也提供给了我们一个方法setdefault,比使用条件判断效率略高,少做了一次dict查询,通过这个方法同样实现上面的功能。
1 2 3 4 5 |
user_dict = {} users = ["aa","bb","cc","aa"] for user in users: user_dict.setdefault(user, 0) #传入key,并给key一个默认值; user_dict[user] += 1 |
以上的方法虽然在一定程度上解决了dict中不存在默认值的问题,但是这时候我们会想,有没有一种字典它本身提供了默认值的功能呢?答案是肯定的,那就是collections.defaultdict。
defaultdict方法默认返回一个字典,会自动给每个键(key)赋一个初始值。这个初始值该赋值什么呢?这就需要defaultdict类的初始化函数接受一个类型作为参数,当所访问的键不存在的时候,可以使用传入的这个类型来实例化一个值作为默认值:
1 2 3 4 5 6 7 8 |
>>> from collections import defaultdict >>> dd = defaultdict(int) >>> print(dd) defaultdict(<class 'int'>, {}) >>> dd['foo'] 0 >>> print(dd) defaultdict(<class 'int'>, {'foo': 0}) |
所以,defaultdict的真正意义实现一种全局的初始化,访问任何键都不会抛KeyError的异常。我们传入的这个类型必须是一个可调用对象,换一种说法就是使用工厂方法default_factory给所有key对应的value赋初始值,这些工厂方法有:int()初始化为0、float()初始化为0.0、complex()初始化为0j、str()初始化为”、list()初始化为[]、tuple()初始化为()、dict()初始化为{}、bool()初始化为False等等。
所以,对于我们上面统计单词出现次数的案例,如果使用defaultdict,只需要你传入一个默认的工厂方法int(),那么请求一个不存在的key时,便会调用这个工厂方法使用其默认值来作为这个key的默认值。另外,defaultdict是使用C语言实现的,效率自然很高。
1 2 3 4 5 |
from collections import defaultdict user_dict = defaultdict(int) #必须传入一个可调用对象,可用内置callable(int)方法测试是否是可调用对象; users = ["aa","bb","cc","aa"] for user in users: user_dict[user] += 1 |
这样一段代码,是不是更简洁了,省去了判断语句或默认方法的使用,使代码的严谨性更高了,出错的机率低了。
上面说了,defaultdict必须传入一个可调用对象名称,无法传入参数的,如果我想生成一些默认的复杂的dict呢?该类除了接受类型名称作为初始化函数的参数之外,还可以使用任何不带参数的可调用函数,到时该函数的返回结果作为默认值,这样使得默认值的取值更加灵活。
如下操作,先生成一个不带参数的函数:
1 2 3 4 5 |
def gen_default(): return { "name": "", "nums": 0 } |
函数是一个可调用对象,所以直接传入defaultdict即可,使用函数的返回值作为默认值:
1 2 3 |
>>> default_dict = defaultdict(gen_default) >>> default_dict["group"] {'nums': 0, 'name': ''} |
这就是defaultdict给我们带来的一些好处。上面只是非常简单的介绍了一下collections模块的主要内容,主要目的就是当你碰到适合使用它们的场所时,能够记起并使用它们,起到事半功倍的效果。
三、deque
deque其实是double-ended queue的缩写,翻译过来就是双端队列,它最大的好处就是实现了从队列头部快速增加和取出对象:.popleft(),.appendleft() 。
你可能会说,原生的list也可以从头部添加和取出对象啊?就像这样:
1 2 3 4 5 6 7 8 |
>>> l = [1,2,3] >>> l.insert(0,10) >>> l [10, 1, 2, 3] >>> l.pop(0) 10 >>> l [1, 2, 3] |
但是值得注意的是,list对象的这两种用法的时间复杂度是O(n) ,也就是说随着元素数量的增加耗时呈线性上升。而使用deque对象则是O(1)的复杂度(C语言写的),所以当你的代码有这样的需求的时候,一定要记得使用deque。
deque初始化对象可以是任意可迭代对象,如下测试:
1 2 3 4 5 6 7 8 9 10 |
>>> from collections import deque >>> user_list = deque(['dkey', 23, 170]) >>> user_list deque(['dkey', 23, 170]) >>> user_list = deque(('dkey', 23, 170)) >>> user_list deque(['dkey', 23, 170]) >>> user_list = deque({'dkey': 23,'andy': 24}) >>> user_list deque(['dkey', 'andy']) |
deque同list一样,一般用来存储相同数据类型,养成良好的编程习惯。这就是跟tuple不同之处,虽然tuple也可以像list这样存储数据(‘dkey’, 23, 170),但一般都把一个tuple看作一个整体,一个对象。
deque支持list类型所有的方法,既然是双端队列,所以deque支持头部和尾部元素添加或删除,且都是(O)1复杂度。
1 2 3 4 5 6 7 8 9 |
>>> user_list = deque([1,2,3]) >>> user_list.append('tail') >>> user_list.appendleft('head') >>> user_list deque(['head', 1, 2, 3, 'tail']) >>> user_list.pop() 'tail' >>> user_list.popleft() 'head' |
并且也提供了extendleft()方法,合并其它可迭代对象到当前对象的头部:
1 2 3 4 5 |
>>> l1 = deque([1,2,3]) >>> l2 = deque([4,5,6]) >>> l1.extendleft(l2) >>> l1 deque([6, 5, 4, 1, 2, 3]) |
作为一个双端队列,deque还提供了一些其他的好用方法,比如rotate等。
举例:下面这个是一个有趣的例子,主要使用了deque的rotate方法来实现了一个无限循环的加载动画。
1 2 3 4 5 6 7 8 9 |
import sys import time from collections import deque fancy_loading = deque('>--------------------') while True: print('\r%s' % ''.join(fancy_loading),) fancy_loading.rotate(1) sys.stdout.flush() time.sleep(0.08) |
一个无尽循环的跑马灯。deque的应用场景还是很多的,比如python内置的queue队列模块中就是使用deque实现的。跟list相比,deque是线程安全的,使用了Python解释器全局锁GLI。而list不是线程安全的,当在实现一些多线程需求时就需要自己加锁了。
四、Counter
计数器是一个非常常用的功能需求,collections也贴心的为你提供了这个功能。
举例:下面这个例子就是使用Counter模块统计一段句子里面所有字符出现次数。
1 2 3 4 |
from collections import Counter s = '''A Counter is a dict subclass for counting hashable objects.'''.lower() c = Counter(s) print(c.most_common(5)) #只输出前5个; |
解析结果如下(默认根据结果从大到小排序):
1 |
[(' ', 9), ('s', 6), ('c', 5), ('a', 5), ('t', 4)] |
同样,Counter接收一个可迭代对象。Counter还提供了一个update方法,可以让用户增加一些统计数据,同样也可以增加Counter。
1 2 3 4 5 6 7 8 |
>>> c.update("scsc") >>> c.most_common(5) [(' ', 9), ('s', 8), ('c', 7), ('a', 5), ('t', 4)] >>> d = Counter("scsc") >>> c.update(d) >>> c.most_common(5) [('s', 10), ('c', 9), (' ', 9), ('a', 5), ('t', 4)] |
还有一些其他方法,比如copy、pop、get、clear等等。
五、OrderedDict
在Python中,dict这个数据结构由于hash的特性,是无序的(这里的无须不是指排序,而是指无法达到字典元素先添加就在前面,后添加就在后面),这在有的时候会给我们带来一些麻烦。幸运的是,collections模块为我们提供了OrderedDict,继承自dict,当你要获得一个有序的字典对象时,用它就对了。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Python 2 >>> from collections import OrderedDict >>> name = dict() >>> name['a'] = "a1" >>> name['b'] = "b2" >>> name['c'] = "c3" >>> print(name) {'a': 'a1', 'c': 'c3', 'b': 'b2'} >>> name = OrderedDict() >>> name['a'] = "a1" >>> name['b'] = "b2" >>> name['c'] = "c3" >>> print(name) OrderedDict([('a', 'a1'), ('b', 'b2'), ('c', 'c3')]) |
需要说明的是,在Python 2中dict是无须的,但是在Python 3中默认已经是有序的了。那么也就是说Python 3中是否不需要使用OrderedDict方法了呢?其实在某些情况下还是需要使用OrderedDict的,既然它继承自dict,自然包含dict所有功能,同时OrderedDict又增加了一些新的方法。比如move_to_end(),支持把一个key移动到字典尾部。
1 2 3 4 |
>>> name = OrderedDict({"a": "a1", "b": "b2", "c": "c3"}) >>> name.move_to_end("a") >>> print(name) OrderedDict([('b', 'b2'), ('c', 'c3'), ('a', 'a1')]) |
六、ChainMap
ChainMap类可把多个字典或者其它映射对象放在一起,组成一个单一的、可更新的映射对象。如果参数没有指定任何映射对象,默认会创建一个空的映射对象。所有传入来的映射对象保存在一个列表里,可以通过maps属性来访问和更新这些映射对象。当在查找时,会查看所有映射对象;但当在写入、更新和删除时,只会操作第一个满足条件的映射对象。ChainMap会通过引用的方式来合并所有映射对象的元素,因此只要任何一个映射对象里的元素进行更新,都会反映到ChainMap对象里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> from collections import ChainMap >>> user_dict1 = {"a": "a1", "b": "b1"} >>> user_dict2 = {"a": "a2", "d": "d1"} >>> new_dict = ChainMap(user_dict1, user_dict2) >>> print(new_dict) ChainMap({'a': 'a1', 'b': 'b1'}, {'a': 'a2', 'd': 'd1'}) # 循环遍历结果; >>> for k, v in new_dict.items(): ... print(k, v) ... a a1 d d1 b b1 |
所以所有字典相同的操作方法都是支持的,另外增加一个maps属性,增加一个创建子字典的方法和访问除了第一个映射对象之外所有其它映射对象的属性。
1 2 3 4 5 6 7 |
# maps属性以列表形式展示数据; >>> new_dict.maps [{'a': 'a1', 'b': 'b1'}, {'a': 'a2', 'd': 'd1'}] # new_child创建子字典; >>> new_dict.new_child({"c": "c1"}) ChainMap({'c': 'c1'}, {'a': 'a1', 'b': 'b1'}, {'a': 'a2', 'd': 'd1'}) |
前面说了可以通过maps属性来访问和更新这些映射对象,也就是更新原始数据。
1 2 3 4 5 6 7 8 |
>>> new_dict.maps[0]["a"] = "aa" >>> new_dict.maps [{'a': 'aa', 'b': 'b1'}, {'a': 'a2', 'd': 'd1'}] >>> user_dict1 {'a': 'aa', 'b': 'b1'} >>> user_dict2 {'a': 'a2', 'd': 'd1'} |
但当在写入、更新和删除时,只会操作第一个满足条件的映射对象。
如果要对它们有一个更全面和深入了解的话,还是建议阅读官方文档和模块源码。
<参考>
collections — Container datatypes