字典dict这种数据结构活跃在所有python程序的背后,即便你的程序没有直接使用它。
字典在python里的作用至关重要,python也对其实现做了高度优化,而背后的散列表则是字典性能出众的根本原因,集合可以看成是只含有键,没有值的特殊的字典。
泛映射类型
你一定听过linux里泛文件抽象的概念吧?即在linux中,一切皆文件。泛映射类型也类似,collections.abc模块中包含Mapping和MutableMapping这两个抽象基类,他们规定了泛映射类型需要满足的一些基本接口
我们一般不会直接继承这些抽象基本,而是根据具体需求对dict或者collections.UserDict进行扩展。
字典有多种构造方法。
1 a = dict(one=1, two=2, three=3) 2 b = {'one':1, 'two':2, 'three':3} 3 c = dict(zip(['one', 'two', 'three'], [1,2,3])) 4 d = dict([('one', 1), ('two', 2), ('three', 3)])
字典推导
有列表推导推广而来,可以方便的构建字典。
1 dialCode = [ 2 (86, 'China'), 3 (91, 'India'), 4 (1, 'USA'), 5 (7, 'Russia'), 6 (81, 'Japan') 7 ] 8 9 d = {country: code for code, country in dialCode} 10 print(d) 11 print(d['USA']) 12 13 d = {country: code for code, country in dialCode if code > 40} 14 print(d)
常见的映射类型方法
映射类型的方法很丰富。
摘自《流畅的python》
在以上的方法里,setdefault是比较微妙的一个。
考虑这样一个问题,在一段英文中查找每个单词出现的位置。
1 import re 2 words = "Mr. Johnson had never been up in an aerophane before and he had read a lot about air accidents, so one day when a friend offered to take him for a ride in his own small phane, Mr. Johnson was very worried about accepting. Finally, however, his friend persuaded him that it was very safe, and Mr. Johnson boarded the plane" 3 4 result = {} 5 word_re = re.compile(r'\w+') 6 for word_match in word_re.finditer(words): 7 word = word_match.group() 8 start = word_match.start() 9 end = word_match.end() 10 val = result.get(word, []) 11 val.append((start,end)) 12 result[word] = val 13 14 print(result)
这段代码没有问题,但是每找到一个单词就要对字典进行两次查找,一次get,一次__setitem__,尝试用setdefault方法解决这个问题。
setdefault接收一个key和一个default=None,当字典中存在key时,就返回key对应的value;否则就以(key, default)为键值对插入字典中,并返回default。
1 result = {} 2 word_re = re.compile(r'\w+') 3 for word_match in word_re.finditer(words): 4 word = word_match.group() 5 start = word_match.start() 6 end = word_match.end() 7 #val = result.get(word, []) 8 #val.append((start,end)) 9 #result[word] = val 10 result.setdefault(word, []).append((start, end)) 11 12 print(result)
可以看到,我们只用了一行代码就完成了查找key、更新key对应的值的功能。
映射的弹性键查询
有的时候,我们要查询的键就算在字典中不存在也希望可以得到一个默认值。有两个方法可以达到这个目的。
1、使用defaultdict
2、自定义一个dict或者UserDict的子类,自己实现__missing__方法
defaultdict
defaultdict位于collections模块中,需要给它提供一个可调用对象default_factory,当要查找的key通过__getitem__找不到时,就会调用该对象,产生一个默认值。
比如,dd = defaultdict(list), 如果'key'在dd中不存在的话,那么list方法被调用,('key', [])作为一对键值对,被插入字典中。
使用defaultdict完成上面setdefault同样的功能
1 from collections import defaultdict 2 result = defaultdict(list) 3 word_re = re.compile(r'\w+') 4 for word_match in word_re.finditer(words): 5 word = word_match.group() 6 start = word_match.start() 7 end = word_match.end() 8 result[word].append((start, end)) 9 10 print(result)
Attention:可调用对象default_factory只会在__getitem__里被调用,其他方法不会有影响。result.get('1234',default=None)返回的依然是None,而不是[]。
特殊方法__missing__
顾名思义,__missing__方法在字典严格点说应该是映射类型找不到键的时候被调用,如果用户自定义了这个方法,那么在__getitem__找不到键的时候就会调用该方法,而不是抛出KeyError异常。
同样的,__missing__只会在__getitem__里被调用,对get或者__contains__(in)没有影响。
如果自定义映射类型,通常都会继承UserDict,而不是dict,主要是因为后者为了提升性能,对一些方法会进行优化,而我们可能就不得不重写这些方法了,UserDict不存在这个问题,而且UserDict不是dict的子类,它有一个dict对象保存在data属性里。
下面我们写一个支持字符串和数字查询、更新、添加的字典类型。
1 from collections import UserDict 2 class Mydict(UserDict): 3 def __missing__(self, key): 4 if isinstance(key, str): #思考为什么此处的判断是必须的呢? 5 raise KeyError 6 return self.data[str(key)] 7 8 def __contains__(self, key): 9 return str(key) in self.data 10 11 def __setitem__(self, key, val): 12 self.data[str(key)] = val 13 mydict = Mydict() 14 mydict[1] = 'one' 15 mydict['2'] = 'two' 16 #print(1 in mydict, '1' in mydict) 17 print(mydict.get(1), mydict.get('1')) 18 print(mydict)
如果没有__missing__中的判断,key是字符串首先到__getitem__中查找不到,然后调用__missing__,__missing__又用self.data[str(key)]的方式又调用__getitem__,如此就会形成无限递归,最终栈溢出。
散列表
散列表其实就是一个稀疏矩阵,总是有空白元素的矩阵被称为稀疏矩阵。散列表里的单元又叫标远,每个表元包含两部分:对键的引用,对值的引用,表元的大小固定,因此通过偏移量来读取表元。
python会设法保证散列表中有大约1/3的表元是空的,所以在快要达到这个阈值的时候,python会把原来的散列表复制到一个更大的空间里。
一个元素要放入散列表,先要计算这个元素的散列值。hash()方法
1、散列值和相等性
hash方法可以计算所有内置类型的散列值,如果是自定义类,需要实现__eq__和__hash__方法。两个对象如果在比较的时候是相等的,那么他们的散列值必须相等,否则散列表不能正常运行。散列值应该在散列表中尽量的分散开。
2、散列表算法
dict[search_key],python首先会计算search_key对象的散列值,然后取较低的几位作为偏移量到散列表中查找表元,如果找到的表元是空的,就会抛出KeyError异常;否则,会拿到表元foundkey:found_value,然后比较foundkey和search是否相等,相等的话,就返回foundvalue,否则,就会发生散列冲突。
散列冲突发生后,python会在散列值中另取几位,重复上述步骤,知道找到对应的value或者抛出异常为止。
往字典里添加元素的时候,python会根据当前散列表里的具体情况决定是否要重新为散列表分配空间,这不是应用程序能控制的,作为程序开发者牢记一点:
不要在迭代字典的过程中对字典添加新键!!
无论任何时候往字典里添加新键,python都有可能做出为散列表扩容的决定,因此散列表中原有键的次序可能会被打乱、以及发生散列冲突,导致的结果则是不可控的,很可能出现意料之外的情况。
集合的特点和字典几乎是完全一致的,这里做一点总结。
* 字典、集合里的元素必须是可散列的
* 字典、集合很消耗内存
* 字典、集合的存取效率很高
* 往字典、集合里添加元素,可能会改变散列表中原有元素的次序