泛映射类型
映射类型:不仅仅是dict,标准库里的所有映射类型都是利用dict来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用做这些映射的键。(只有键有这个需求,值并不需要必须是可散列的数据类型。)
什么是可散列的数据类型?
可散列的对象在它的生命周期中,散列值是不变的,需要实现hash()方法。另外散列对象还要有eq()方法,这样才能跟其他键作比较。 – 原子不可变数据类型都是可散列的(str,bytes和数值类型,frozenset) – dict,list是不可散列的
用setdefault处理找不到的键
当字典d[k]找不到值会抛出异常,通常我们使用d.get(k,default)来代替d[k],给找不到的键默认一个返回值。 但是要更新某个键对应的值的时候,不管是用getitem还是get都不太自然的,效率很低。
这样使用 setdefault
my_dict.setdefault(key,[]).append(new_value)
跟这样写使用默认的dict
if key not in my_dict:
my_dict[key]=[]
my_dict[key].append(new_value)
两者的效果是一样的,只不过后者至少要进行两次查询——如果键不存在的话,就是三次,使用setdefault只需要一次就可以完成整个操作。
映射的弹性查询
所谓的弹性查询就是,我找的键不在映射里面存在的时候,也能返回一个默认值比如
d.get(key,default)
python有两个途径达到整个目的, – 一个是通过defaultdict这个类型而不是普通的dict – 另一个是给自己顶一个dict的子类,然后在子类中实现missing方法。
defaultdict:处理找不到的键的一个选择
dd = defaultdict(list)
print(dd[‘new-key’]) # []
“””
调用list()来建立一个列表。
把这个新列表作为值,’new-key’作为它的键,放到dd中。
返回这个列表的引用。
“””
print(dd) #defaultdict(<class ‘list’>, {‘dddd’: []})
注意如果在创建defaultdict的时候没有指定default_factory,查询不存在键会触发KeyError
特殊方法missing
所有的映射类型找不到键的时候,都会使用到missing方法。 虽然基类dict并没有定义这个方法,但是dict知道有这么个东西的存在。 也就是说,如果有一个类继承了dict,然后这个继承类提供了missing方法, 那么在getitem碰到找不到键的时候,python会自动调用它,而不是抛出一个KeyError异常。 missing方法只会被getitem调用
字典的变种
collections.OrderedDict:这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。
collections.ChainMap:该类型可以容纳数个不同的对象,然后在进行键查找操作的时候,这些对象会被当做一个整体逐个被查找。
import collections
初始化字典
dict1 = {‘a’: 1, ‘b’: 2}
dict2 = {‘b’: 3, ‘c’: 4}
初始化ChainMap
chain = collections.ChainMap(dict1, dict2)
使用maps输出chainMap
print(chain.maps) # [{‘b’: 2, ‘a’: 1}, {‘b’: 3, ‘c’: 4}]
输出key
print(list(chain.keys())) # [‘b’, ‘c’, ‘a’]
输出值
print(list(chain.values())) # [2, 4, 1]
访问
print(chain[‘b’]) # 2
print(chain.get(‘b’)) # 2
使用new_child添加新字典
dict3 = {‘f’: 5}
new_chain = chain.new_child(dict3)
print(new_chain.maps) # [{‘f’: 5}, {‘b’: 2, ‘a’: 1}, {‘b’: 3, ‘c’: 4}]
reversed(new_chain.maps)
print(new_chain.maps)
collections.Counter:这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。
collections.UserDict:把标准的dict用纯python又实现了一遍。
不可变的映射类型
标准库里所有的映射类型都是可变的,如果遇到不能让用户错误的修改某个映射。 使用types.MappingProxyType,如果给这个类一个映射,它会返回一个只读的映射视图(动态的)。
集合
相对dict,set这个概念在python算是比较年轻的,有set 跟 frozenset 集合的本质是许多唯一对象的聚集,所以集合中的元素必须都是可散列的,set类型本身是不可散列的,但是frozenset时可散列的。 集合可以进行中缀运算符。
dict和set的背后
python里的dict和set的效率有多高?
为什么它们是无序的?
为什么并不是所有的python对象都可以当做dict的键或者是set的元素?
为什么dict的键和set元素的顺序是根据他们被添加的次序而定的,以及为什么在映射对象的生命周期中,这个顺序是一成不变的?
为什么不应该在迭代循环dict或者是set的同时往里添加元素?
字典中的散列表
散列表其实是一个稀疏数组(总是有空白元素的数组成为稀疏数组) 散列表里的单元通常叫做表元(bucket),在dict的散列表中每个键值对都占用一个表元,每个表元分都有两个部分,一个是对键的引用,一个是对值的引用。 如果把对象放到散列表,那么首先要计算这个元素键的散列值,python中可以用hash()方法来做这个事情。
散列值和相等性
如果1==1.0为真,那么`hash(1)==hash(1.0)也必须为真
散列表算法
为了获取my_dict[search_key]背后的值,Python首先调用hash(search_key)来计算search_key的散列值,把这个值最低的几位数字当做偏移量,在散列表里查找表元(具体取几位,得看当前散列表的大小)。 若找到的表元为空的,则抛出KeyError异常。若不为空的,则表元里会有一对found_key:found_value。 这时候python会检验search_key == found_key是否为真,如果它们相等,就回返回found_value。 如果search_key和found_key不匹配的话,这种情况称为散列冲突。 发生原因是散列表所做的其实是把随机的元素映射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字的一部分。 为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊方法处理一下,把新的到的数据在当做索引来寻找表元。
问题:如果定位一个表元?[^2]
dict的实现及其导致的结果
散列表带给dict的优势和限制。
键必须是可散列的
一个可散列的对象必须满足以下的需求: – 支持hash()函数,并且通过hash()方法所得到的散列值是不变的。 – 支持通过eq()方法来检测相等性。 – 若 a == b为真,则hash(a) == hash(b)也为真。
用户自定义的对象默认是可散列的,它们的散列值有id()来获取。
字典在内存上面开销巨大
通常不需要优化,如果数据量巨大考虑使用tuple()来替代dict()
特殊方法slots
键查询很快
dict的实现是典型的空间换时间
键的次序取决于添加顺序
当往dict中添加新建而又发生散列冲突的时候,新建可能会被安排存放在另个一个位置。
dict([key1,value1],[key2,value2]) == dict([key2,value2],[key1,value1]) # true
虽然键的次序是乱的,但是被视作相等的。这就是为什么说字典是无序的原因。
往字典中添加新建可能会改变已有键的顺序
无论何时往字典添加新的键,Python的解释器都可能做出为字典扩容的决定。 扩容导致的结果是需要一个更大散列表,并把字典里已有的元素添加到新表里,这个过程就可能出现散列冲突,导致新的散列表中的键的次序变化。
所以不要对字典同时迭代和修改。 python3中的.keys() .items() 和.values()方法返回都是字典的视图,这些方法返回的值更像是集合。
set的实现以及导致的结果可以参照没有值dict,python学习交流群 984632579 除了分享技术文章之外还有很多福利,加群领取学习资料可以领取包括不限于Python实战演练、PDF电子文档、面试集锦、学习资料等。