Python源码剖析——Dict对象
前言
现代的编程语言都在语言级或标准库中提供某种关联式的容器
,容器中的元素通常是以(key,value)的形式存在的。
关联式容器总会极大地关注键的搜索效率,其实现都会基于良好的数据结构,如C++中的map的实现基于红黑树,搜索时间复杂度为O(logN)。
Python中的关联容器PyDictObject
即dict是基于散列表
实现的。Dict对搜索的效率要求很高,因为Python本身的实现中dict被大量的使用,如Python字节码的运行环境,其中存放变量名和变量值,通过查找变量名获得变量值。因此dict的实现没有采用平衡二叉树,而是采用了散列表,最优情况下散列表的搜索复杂度为O(1)。
散列表
Python在处理哈希冲突的时候采用开放地址法
,在删除散列表中的元素的时候,不能进行真正的删除,因为会导致冲突探测链的探测过早的结束,所以Python采用一种伪删除
的方式。
PyDictObject
关联容器的entry
关联容器中的一个(key,value)元素称为一个entry
或slot。Python中,一个entry定义如下:
|
|
PyDictObject存放的是PyObject*,可以存放任何Python对象。me_hash
域存储的是me_key
的散列值。
PyDictObject中entry可以在3种状态间转换:
Unused态
:该entry从现在到之前都没有存储过(key,value)对。Active态
:存储了一个(key,value)对entry便传化为Active态。Dummy态
: entry中的(key,value)对被“伪删除”后,进入Dummy态。
关联容器的实现
关联容器PyDictObject是一大堆entry的集合,集合结构如下:
当PyDictObject中的entry数量超过8
个时,Python会认为这是一个大dict了,将会申请额外内存空间,并将ma_table
指向这块空间。
PyDictObject中元素搜索
PyDictObject提供两种搜索策略,lookdict
和lookdict_string
,Python默认搜索方式为lookdict_string
。
Python获取冲突链上的第一个entry的过程采用,将hash值(我认为是元素序号)与entry的数量做一个与操作,结果自然会落在entry的数量之下。
freeslot
正是用来指向探测序列中第一个处于Dummy态的entry,如果搜索失败,freeslot会提供一个处于Dummy态的entry。
根据hash值获得的冲突探测链上第一个entry
与待查找的元素的比较:
- 根据hash值获得entry的索引,是冲突探测链上的第一个entry的索引。
- 在两种情况下搜索结束:
- entry处于Unused态,表明冲突探测链搜索完成,搜索失败;
- ep->me_key == key,表明entry的key与待搜索的key匹配,搜索成功。
- 若当前entry处于Dummy态,这是freeslot。
- 检查Active态entry中的key与待查找的key是否“值相同”,若成立,搜索成功。
遍历探测链
时发生的lookdict所进行的操作:
- 根据Python所采用的探测函数,获得探测链中的下一个待检查的entry。
- 检查到一个Unused态entry,表明搜索失败,这时有两种结果:
- 如果freeslot不为空,则返回freeslot所指entry。
- 如果freeslot为空,则返回该Unused态entry。
- 检查entry中的key与带查找的key是否符合“引用相同”规则。
- 检查entry中的key与待查找的key是否符合“值相同”规则。
- 遍历过程中,如果发现Dummy态entry,且freeslot未设置,则设置freeslot。
PyDictObject插入
搜索成功,返回处于Active的entry,直接替换me_value。
搜索失败,返回Unused或Dummy的entry,完整设置me_key、me_hash和me_value。
如果table的装载率大于2/3
时,后续的插入动作遭遇到冲突的可能性会非常大,在装载率大于或等于2/3
时需要改变table的大小。
PyDictObject的删除
先计算hash值,搜索相应的entry,删除entry维护的元素,将entry从Active态变换为Dummy态,调整PyDictObject对象中维护table使用情况的变量。