Python中字典跟集合
泛映射类型
映射类型:不仅仅是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()
方法返回都是字典的视图,这些方法返回的值更像是集合。