泛映射类型
collections.abc 模块中有 Mapping 和 MutableMapping 这两个抽象基类,它们的作用是为 dict 和其他类似的类型定义形式接口(在Python 2.6 到 Python 3.2 的版本中,这些类还不属于 collections.abc模块,而是隶属于 collections 模块)。详见图
collections.abc 中的 MutableMapping 和它的超类的UML 类图(箭头从子类指向超类,抽象类和抽象方法的名称以斜体显示)
然而,非抽象映射类型一般不会直接继承这些抽象基类,它们会直接对dict 或是 collections.User.Dict 进行扩展。这些抽象基类的主要作用是作为形式化的文档,它们定义了构建一个映射类型所需要的最基本的接口。然后它们还可以跟 isinstance 一起被用来判定某个数据是不是广义上的映射类型:
>>> my_dict = {} >>> isinstance(my_dict, abc.Mapping) True
原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是可散列的,因为根据其定义,frozenset 里只能容纳可散列类型。元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。来看下面的元组tt、tl 和 tf:
>>> tt = (1, 2, (30, 40)) >>> hash(tt) 8027212646858338501 >>> t1 = (1, 2, [30, 40]) >>> hash(t1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' >>> tf = (1, 2, frozenset([30, 40])) >>> hash(tf) -4118419923444501110
举个? 来说明创建字典的不同方式:
>>> a = dict(one=1, two=2, three=3) >>> b = {'one': 1, 'two': 2, 'three': 3} >>> c = dict(zip(['one', 'two', 'three'], [1, 2, 3])) >>> d = dict([('two', 2), ('one', 1), ('three', 3)]) >>> e = dict({'three': 3, 'one': 1, 'two': 2}) >>> a == b == c == d == e True
字典推导
字典推到式的应用
1 DIAL_CODES = [ 2 (86, 'China'), 3 (91, 'India'), 4 (1, 'United States'), 5 (62, 'Indonesia'), 6 (55, 'Brazil'), 7 (92, 'Pakistan'), 8 (880, 'Bangladesh'), 9 (234, 'Nigeria'), 10 (7, 'Russia'), 11 (81, 'Japan'), 12 ] 13 14 country_code = {country: code for code, country in DIAL_CODES} 15 print(country_code) 16 17 #过滤国家编号小于66的所有信息 18 r1 = {country: code for country, code in country_code.items() if code < 66 } 19 print(r1) 20 21 r2 = filter(lambda x:x[1] < 66, country_code.items()) 22 print(dict(r2))
以上代码执行的结果为:
{'China': 86, 'India': 91, 'United States': 1, 'Indonesia': 62, 'Brazil': 55, 'Pakistan': 92, 'Bangladesh': 880, 'Nigeria': 234, 'Russia': 7, 'Japan': 81} {'United States': 1, 'Indonesia': 62, 'Brazil': 55, 'Russia': 7} {'United States': 1, 'Indonesia': 62, 'Brazil': 55, 'Russia': 7}
用setdefault处理找不到的键
当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉的“快速失败”哲学。也许每个 Python 程序员都知道可以用 d.get(k, default) 来代替 d[k],给找不到的键一个默认的返回值(这比处理 KeyError 要方便不少)。但是要更新某个键对应的值的时候,不管使用 __getitem__ 还是 get 都会不自然,而且效率低。
举个? index0.py 这段程序从索引中获取单词出现的频率信息,并把它们写进对应的列表里
1 import sys 2 import re 3 4 WORD_RE = re.compile(r'\w+') 5 6 index = {} 7 with open(sys.argv[1], encoding='utf-8') as fp: 8 for line_no, line in enumerate(fp, 1): 9 for match in WORD_RE.finditer(line): 10 word = match.group() 11 column_no = match.start() + 1 12 location = (line_no, column_no) 13 # 这其实是一种很不好的实现,这样写只是为了证明论点 14 occurrences = index.get(word, []) #提取 word 出现的情况,如果还没有它的记录,返回[] 15 occurrences.append(location) #把单词新出现的位置添加到列表的后面 16 index[word] = occurrences #把新的列表放回字典中,这又牵扯到一次查询操作 17 # 以字母顺序打印出结果 #排序的时候,单词会被规范成统一格式 18 for word in sorted(index, key=str.upper): 19 print(word, index[word])
以上代码执行的结果为:
$ python3 index0.py ../../data/zen.txt a [(19, 48), (20, 53)] Although [(11, 1), (16, 1), (18, 1)] ambiguity [(14, 16)] and [(15, 23)] are [(21, 12)] aren [(10, 15)] at [(16, 38)] bad [(19, 50)] be [(15, 14), (16, 27), (20, 50)] beats [(11, 23)] Beautiful [(3, 1)] better [(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8), (18, 25)] ...
index.py 用一行就解决了获取和更新单词的出现情况列表,当然跟示例 3-2 不一样的是,这里用到了 dict.setdefault
1 import sys 2 import re 3 4 5 WORD_RE = re.compile(r'\w+') 6 7 index = {} 8 with open(sys.argv[1], encoding='utf-8') as fp: 9 for line_no, line in enumerate(fp, 1): 10 for match in WORD_RE.finditer(line): 11 word = match.group() 12 column_no = match.start() + 1 13 location = (line_no, column_no) 14 ''' 15 获取单词的出现情况列表,如果单词不存在,把单词和一个空列表放进映射,然后返回这个空列表, 16 这样就能在不进行第二次查找的情况下更新列表了 17 ''' 18 index.setdefault(word, []).append(location) 19 20 # 以字母顺序打印出结果 21 for word in sorted(index, key=str.upper): 22 print(word, index[word]) 23
获取单词的出现情况列表,如果单词不存在,把单词和一个空列表放进映射,然后返回这个空列表,这样就能在不进行第二次查找的情况下更新列表了。
也就是说,这样写:
1 my_dict = {'name': 'demon'} 2 my_dict.setdefault('course', []).append('python') 3 4 print(my_dict) 5 6 #等同于上面的写法,不过这个要查询两次 7 if 'course' not in my_dict: 8 my_dict['course'] = [] 9 my_dict['course'].append('golang') 10 print(my_dict)
二者的效果是一样的,只不过后者至少要进行两次键查询——如果键不存在的话,就是三次,用 setdefault 只需要一次就可以完成整个操作。
特殊方法__missing__
所有的映射类型在处理找不到的键的时候,都会牵扯到 __missing__方法。这也是这个方法称作“missing”的原因。虽然基类 dict 并没有定义这个方法,但是 dict 是知道有这么个东西存在的。也就是说,如果有一个类继承了 dict,然后这个继承类提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。
__missing__ 方法只会被 __getitem__ 调用(比如在表达式 d[k] 中)。提供 __missing__ 方法对 get 或者__contains__(in 运算符会用到这个方法)这些方法的使用没有影响。这也是我在上一节最后的警告中提到,defaultdict 中的default_factory 只对 __getitem__ 有作用的原因。
举个? StrKeyDict0 在查询的时候把非字符串的键转换为字符串
1 #StrKeyDict0 继承了 dict 2 class StrKeyDict0(dict): 3 4 #如果找不到的键本身就是字符串,那就抛出 KeyError 异常。 5 def __missing__(self, key): 6 if isinstance(key, str): 7 raise KeyError(key) 8 #如果找不到的键不是字符串,那么把它转换成字符串再进行查找(递归查询),会调用__getitem__ 9 return self[str(key)] 10 11 #get 方法把查找工作用 self[key] 的形式委托给 __getitem__ 12 def get(self, key, default=None): 13 try: 14 return self[key] 15 except KeyError: 16 return default 17 18 def __contains__(self, key): 19 return key in self.keys() or str(key) in self.keys() 20 21 d = StrKeyDict0([('2', 'two'), ('4', 'four')]) 22 print(d['2']) 23 print(d['4'])
字典的变种
collections.OrderedDict
这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。
collections.Counter
这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。Counter 实现了 + 和 - 运算符用来合并记录,还有像most_common([n]) 这类很有用的方法。most_common([n]) 会按照次序返回映射里最常见的 n 个键和它们的计数。
举个?
1 from collections import Counter 2 3 4 #创建一个计数的对象 5 ct = Counter('abracadabra') 6 print(ct) 7 8 #更新 9 ct.update('aaageagaege') 10 11 print(ct) 12 13 #打印最多的两个 14 print(ct.most_common(2))
以上代码执行的结果为:
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) Counter({'a': 10, 'g': 3, 'e': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) [('a', 10), ('g', 3)]
子类化UserDict
就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的dict 为基类要来得方便。
无论是添加、更新还是查询操作,StrKeyDict 都会把非字符串的键转换为字符串。
1 import collections 2 3 4 class StrKeyDict(collections.UserDict): 5 6 def __missing__(self, key): 7 if isinstance(key, str): 8 raise KeyError(key) 9 return self[str(key)] 10 11 def __contains__(self, key): 12 return key in self.keys() or str(key) in self.keys() 13 14 #__setitem__ 会把所有的键都转换成字符串 15 def __setitem__(self, key, item): 16 self.data[str(key)] = item
因为 UserDict 继承的是 MutableMapping,所以 StrKeyDict 里剩下的那些映射类型的方法都是从 UserDict、MutableMapping 和Mapping 这些超类继承而来的。特别是最后的 Mapping 类,它虽然是一个抽象基类(ABC),但它却提供了好几个实用的方法。以下两个方法值得关注。
MutableMapping.update
这个方法不但可以为我们所直接利用,它还用在 __init__ 里,让构造方法可以利用传入的各种参数(其他映射类型、元素是 (key,value) 对的可迭代对象和键值参数)来新建实例。因为这个方法在背后是用 self[key] = value 来添加新值的,所以它其实是在使用我们的 __setitem__ 方法。
Mapping.get
在 StrKeyDict0中,我们不得不改写 get 方法,好让它的表现跟 __getitem__ 一致。
不可变映射类型
从 Python 3.3 开始,types 模块中引入了一个封装类名叫MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改。
举个? 用 MappingProxyType 来获取字典的只读实例
>>> from types import MappingProxyType >>> d = {'1': 'A'} >>> d_proxy = MappingProxyType(d) >>> d_proxy mappingproxy({'1': 'A'}) >>> d_proxy['2'] = 'b' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'mappingproxy' object does not support item assignment >>> d['2'] = 'b' >>> d_proxy['2'] 'b'
集合论
“集”这个概念在 Python 中算是比较年轻的,同时它的使用率也比较低。set 和它的不可变的姊妹类型 frozenset 直到 Python 2.3 才首次以模块的形式出现,然后在 Python 2.6 中它们升级成为内置类型。
集合的本质是许多唯一对象的聚集。因此,集合可以用于去重:
>>> l = ['spam', 'spam', 'eggs', 'spam'] >>> set(l) {'eggs', 'spam'} >>> list(set(l)) ['eggs', 'spam']
集合字面量
除空集之外,集合的字面量——{1}、{1, 2},等等——看起来跟它的数学形式一模一样。如果是空集,那么必须写成 set() 的形式。
在 python 3 里面,除了空集,集合的字符串表示形式总是以 {...} 的形式出现。
?
>>> s = {1} >>> type(s) <class 'set'> >>> s {1} >>> s.pop() 1 >>> s set()
由于 Python 里没有针对 frozenset 的特殊字面量句法,我们只能采用构造方法。Python 3 里 frozenset 的标准字符串表示形式看起来就像构造方法调用一样。来看这段控制台对话:
>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
集合推导
举个? 新建一个 Latin-1 字符集合,该集合里的每个字符的Unicode 名字里都有“SIGN”这个单词
>>> from unicodedata import name >>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} {'°', '#', '¶', 'µ', '£', '%', '§', '¤', '©', '<', '+', '±', '®', '¬', '¥', '÷', '¢', '>', '×', '$', '='}
集合的操作
>>> a = set((1,2,3,4,5)) >>> a {1, 2, 3, 4, 5} >>> b = set((4,5,6,7,8,10)) >>> a {1, 2, 3, 4, 5} >>> b {4, 5, 6, 7, 8, 10} >>> a - b {1, 2, 3} >>> b - a {8, 10, 6, 7} >>> a | b {1, 2, 3, 4, 5, 6, 7, 8, 10} >>> a & b {4, 5} >>> a ^ b {1, 2, 3, 6, 7, 8, 10}
常用的操作:
- 交集:a & b
- 并集:a | b
- 差集:a - b
- 对称差集:a ^ b