以下内容是针对:python源码剖析中的第五章——python中Dict对象 的读书笔记(针对书中讲到的内容进行了自己的整理,并且针对部分内容根据自己的需求进行了扩展)
一、Dict的用法
Dict的对象在使用到了所谓的关联关系的时候,就是通过key-value的形式,能够通过key值快速定位到某个value值;
Dict的相关操作如下:
class mydict(object):
def __init__(self):
self.d = {}
def fuzhi(self):
self.d = {'2':23, '3':'34'}
def fuzhi2(self):
self.d[1] = 1
self.d['str'] = 123
self.d['may'] = 234
self.d['may'] = 789
def delkey(self):
del(self.d[1]) #删除key时,key值必须存在于dict中,否则会报出 KeyError
self.d.pop('str')
def bianli(self):
for (k,v) in self.d.items():
print k, v
def getkeysandv(self):
print self.d.keys() #获取当前dict中的所有key值
#获取某个元素值:
print self.d.get('345') #若key不存在,则返回默认返回值None
print self.d.get('123', -1) #若key不存在,则返回自定义的返回值-1
print self.d.get('may') #若key值存在,则返回对应的value
def setkeyexcept(self):
self.d[[1,2,3]] =123 #可变对象不能作为key,会提示:TypeError: unhashable type: 'list' 的类似错误
二、Dict的存储实现原理
python中的dict对象也即PyDictObject对象,因为对搜索的效率要求很高,所以选择了散列表(hash table),因为在最优情况下,散列表能够提供O(1)的搜索效率
因此:这里就能想到在leetcode上面刷的题目中,很多通过list形式可以实现的,为了降低时间复杂度,可以用hash的方式,选择dict对象存储(当然具体问题要具体分析)
散列表的基本思想是:通过一定的函数将需要搜索的键值映射为一个整数,根据这个整数作为索引去访问某片连续的内存区域。用于映射的函数称为映射函数,映射所产生的值称为散列值(hash value)。散列函数对搜索效率有直接的决定性作用。在使用散列函数将不同的值可能映射到相同的散列值,这个时候就需要冲突解决(装载率大于2/3时,冲突的概率就会大大增加)
冲突解决在python中使用的是开放定址法,就是通过一个二次探测函数f,计算下一个位置,一直到找到下一个可用的位置为止,在这个过程中会到达多个位置,这些位置就形成了一个“冲突探测链”,这个冲突探测链在查找某个元素的时候起到重要作用,所以在删除某个位置上的元素,不能直接将这个位置的内容删除,如果删除的话,则导致后续依赖于这个位置的其他值就都无法寻找到了,所以只能进行“伪删除”(通过给元素设置状态,dummy态,表示没有存储具体的值但是还会用到的废弃态)
具体的PyDictObject对象中,会存在每一个元素,元素的定义是:
typedef struct{
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
}PyDictEntry;
每一个元素有三种状态,分别是:unused、active、dummy
状态 |
具体含义 |
unused |
me_key=Null,me_value=Null |
active |
me_key!=Null,me_value!=Null |
dummy |
me_key=dummy,me_value=Null |
PyDictObject对象的定义:
#define PyDict_MINSIZE 8
typedef struct _dictobject PyDictObject;
struct _dictobject{
PyObject_HEAD
Py_ssize_t ma_fill; //元素个数: active+dummy
Py_ssize_t ma_used; //元素个数: Active
Py_ssize_t ma_mask;
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup) (PyDictEntry *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
}
其中各个字段的含义如下:ma_fill域中维护着从PyDictObject对象创建开始直到现在为止所有曾经及正在处于active态的entry个数;ma_used域中维护着当前正处于Active态的entry个数;ma_smalltable的PyDictEntry的数组,表示的是当创建一个PyDictObject对象的时候,至少创建PyDict_MINISIZE个entry被创建(这个数量在程序中定义是8个,这个数量是经过长期的经验所得来的值);ma_table是指向一块区域的,如果entry的数量不超过8个,那么这个指针就指向了ma_smalltable的地址,如果超过了8个,就需要申请一块大的内存,并且ma_table指向这块大的内存
三、Dict的操作实现原理(包括插入、删除、以及缓冲池等)
首先介绍:PyDictObject对象的元素搜索策略:
有两种搜索策略,分别是lookdict和lookdict_string,lookdict_string就是lookdict在对于PyStringObject进行搜索时的特殊形式,那么通用的搜索策略lookdict的主要逻辑是:
(1)对第一个entry的查找:
a)根据hash值获得entry的索引
b)若entry处于unused态,则搜索结束;若entry所指向的key与搜索的key相同,则搜索成功
c)若当前entry处于dummy态,则设置freeslot(这里的freeslot是可以返回作为下一个立即可用的地址来存储entry)
d)检查Active态的entry,若其key所指向的值与搜索的值相同,则搜索成功
(2)对剩余的探测链中的元素的遍历查找:
a)根据所采用的探测函数,获得探测链上的下一个待检查的entry
b)检查到一个unused态的entry,表明搜索失败:
如果freeslot不为空,则返回freeslot;否则返回unused态的entry
c)检查entry的key与所搜索的key的引用是否相同,相同则搜索成功,返回entry
d)检查entry的key与所搜索的key的值是否相同,相同则搜索成功,返回entry
e)遍历过程中,发现dummy态的entry,且freeslot未设置,则设置freeslot
接下来是:PyDictObject对象的元素插入与删除的策略:
需要首先用到搜索策略,搜索成功,则直接将值进行替换,搜索失败,返回unused态或dummy态的entry,设置key、value和hash值,并且根据目前插入的元素情况进行ma_table的大小的调整(调整的依据就是装载率,根据是否大于2/3来进行调整);删除也是类似,先计算hash值,然后搜索相应的entry,搜索成功,删除entry中维护的元素,将entry从Active态修改为dummy态
在PyDictObject的实现过程中,会用到缓冲池,在PyDictObject对象被销毁的时候,才开始接纳被缓冲的PyDictObject对象,定义的缓冲池可接纳的对象数量是80个,创建新PyDictObject对象的时候,如果缓冲池中有,则可以直接从缓冲池中取出使用