本小节讲解dict的abc继承关系
,根据继承关系,如果想要实现一个dict数据结构,需要实现Mapping
类对应的魔法函数。dict子类
中将当我们需要继承dict时候,不应该继承python原生dict,而应该继承collections
下提供UserDict
类。最后介绍dict的实现原理,已经dict的优缺点。
1.dict的abc继承关系
dict协议这里主要根据abc
(抽象基类)的继承关系来理解。
import collections.abc
# abc模块
__all__ = ["Awaitable", "Coroutine",
"AsyncIterable", "AsyncIterator", "AsyncGenerator",
"Hashable", "Iterable", "Iterator", "Generator", "Reversible",
"Sized", "Container", "Callable", "Collection",
"Set", "MutableSet",
"Mapping", "MutableMapping",
"MappingView", "KeysView", "ItemsView", "ValuesView",
"Sequence", "MutableSequence",
"ByteString",
]
collections.abc 工具类中有Mapping
(不可变Map)和MutableMapping
(可变Map)。dict属于Mapping类型。
# Collection 源码
class Collection(Sized, Iterable, Container):
...
...
# Mapping 源码
class Mapping(Collection):
__slots__ = ()
"""A Mapping is a generic container for associating key/value
pairs.
This class provides concrete generic implementations of all
methods except for __getitem__, __iter__, and __len__.
"""
@abstractmethod
def __getitem__(self, key):
raise KeyError
...
...
# MutableMapping 源码
class MutableMapping(Mapping):
__slots__ = ()
"""A MutableMapping is a generic container for associating
key/value pairs.
This class provides concrete generic implementations of all
methods except for __getitem__, __setitem__, __delitem__,
__iter__, and __len__.
"""
@abstractmethod
def __setitem__(self, key, value):
raise KeyError
@abstractmethod
def __delitem__(self, key):
raise KeyError
...
...
从上面类的继承关系中,MutableMapping
是继承Mapping
类,Mapping类继承Collection
,Collenction继承Sized
、Iterable
、Container
。Sized类中主要实现__len__
方法,Iterable类中主要实现__iter__
方法,Container类中主要实现__contains__
方法。所以一个dict实例可以使用len
函数获取dict对象的长度,使用for in
可以遍历dict对象,使用in
可以判断dict对象。
MutableMapping继承了Mapping类,在Mapping基础上添加__setitem__
、__delitem__
方法。
from collections.abc import Mapping
a = {'name':'张三',age:18,sex:'男'}
print(len(a))
>>> 3
for i in a:
print(i)
print('name' in di)
>>> True
# dict对象并不是继承Mapping类,而是实现Mapping类中的协议
print( isinstance(di, Mapping))
>>> True
2.dict的子类
python中内置的一些数据类型是由C语言写,这样可以提升python的效率。对于内置的一些类dict
、list
不建议直接继承这些类。python中有个collections
模块中对C写的内置的数据类型用python重新写了一遍,逻辑都是一样的,建议继承这些重新写过的类。
import collections
__all__ = ['deque', 'defaultdict', 'namedtuple', 'UserDict', 'UserList',
'UserString', 'Counter', 'OrderedDict', 'ChainMap']
继承原生Dict没有作用
class Mydict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, value * 2)
mydict = Mydict()
mydict['age'] = 5
print(mydict)
继承UserDict
from collections import UserDict
class Mydict(UserDict):
def __setitem__(self, key, value):
super().__setitem__(key, value * 2)
mydict = Mydict()
mydict['age'] = 5
print(mydict)
3.dict实现原理
原文 Python字典对象实现原理
python dict和list性能测试
上面dict和list性能测试得出下面结论:
- dict查找数据性能远远大于list
- 在list中随着list数据增大 查找时间也会增大
- 在dict中查找元素不会随着dict的增大而增大
哈希表 (HASH TABLES)
哈希表(也叫散列表),根据关键值对(Key-value)而直接进行访问的数据结构。它通过把key和value映射到表中一个位置来访问记录,这种查询速度非常快,更新也快。而这个映射函数叫做哈希函数,存放值的数组叫做哈希表。 哈希函数的实现方式决定了哈希表的搜索效率。具体操作过程是:
数据添加:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。 数据查询:再次使用哈希函数将key转换为对应的数组下标,并定位到数组的位置获取value。 但是,对key进行hash的时候,不同的key可能hash出来的结果是一样的,尤其是数据量增多的时候,这个问题叫做哈希冲突。如果解决这种冲突情况呢?通常的做法有两种,一种是链接法,另一种是开放寻址法,Python选择后者。
开放寻址法(OPEN ADDRESSING)
开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。
PYDICTENTRY
字典中的一个key-value键值对元素称为entry(也叫做slots),对应到Python内部是PyDictEntry,PyDictObject就是PyDictEntry的集合。PyDictEntry的定义是:
typedef struct {
/* Cached hash code of me_key. Note that hash codes are C longs.
* We have to use Py_ssize_t instead because dict_popitem() abuses
* me_hash to hold a search finger.
*/
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;
me_hash用于缓存me_key的哈希值,防止每次查询时都要计算哈希值,entry有三种状态。
- Unused: me_key == me_value == NULL; Unused是entry的初始状态,key和value都为NULL。插入元素时,Unused状态转换成Active状态。这是me_key为NULL的唯一情况。
- Active: me_key != NULL and me_key != dummy 且 me_value != NULL; 插入元素后,entry就成了Active状态,这是me_value唯一不为NULL的情况,删除元素时Active状态刻转换成Dummy状态。
- Dummy: me_key == dummy 且 me_value == NULL; 此处的dummy对象实际上一个PyStringObject对象,仅作为指示标志。Dummy状态的元素可以在插入元素的时候将它变成Active状态,但它不可能再变成Unused状态。
为什么entry有Dummy状态呢?这是因为采用开放寻址法中,遇到哈希冲突时会找到下一个合适的位置,例如某元素经过哈希计算应该插入到A处,但是此时A处有元素的,通过探测函数计算得到下一个位置B,仍然有元素,直到找到位置C为止,此时ABC构成了探测链,查找元素时如果hash值相同,那么也是顺着这条探测链不断往后找,当删除探测链中的某个元素时,比如B,如果直接把B从哈希表中移除,即变成Unused状态,那么C就不可能再找到了,因为AC之间出现了断裂的现象,正是如此才出现了第三种状态—Dummy,Dummy是一种类似的伪删除方式,保证探测链的连续性。
PYDICTOBJECT
PyDictObject就是PyDictEntry对象的集合,PyDictObject的结构是:
typedef struct _dictobject PyDictObject;
struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill; /* # Active + # Dummy */
Py_ssize_t ma_used; /* # Active */
/* The table contains ma_mask + 1 slots, and that's a power of 2.
* We store the mask instead of the size because the mask is more
* frequently needed.
*/
Py_ssize_t ma_mask;
/* ma_table points to ma_smalltable for small tables, else to
* additional malloc'ed memory. ma_table is never NULL! This rule
* saves repeated runtime null-tests in the workhorse getitem and
* setitem calls.
*/
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};
变量含义:
- ma_fill :所有处于Active以及Dummy的元素个数
- ma_used :所有处于Active状态的元素个数
- ma_mask :所有entry的元素个数(Active+Dummy+Unused)
- ma_smalltable:创建字典对象时,一定会创建一个大小为PyDict_MINSIZE==8的PyDictEntry数组。
- ma_table:当entry数量小于PyDict_MINSIZE,ma_table指向ma_smalltable的首地址,当entry数量大于8时,Python把它当做一个大字典来处理,此刻会申请额外的内存空间,同时将ma_table指向这块空间。
- ma_lookup:字典元素的搜索策略
PyDictObject使用PyObject_HEAD而不是PyObject_Var_HEAD,虽然字典也是变长对象,但此处并不是通过ob_size来存储字典中元素的长度,而是通过ma_used字段。
PYDICTOBJECT的创建过程
PyObject *
PyDict_New(void)
{
register PyDictObject *mp;
if (dummy == NULL) { /* Auto-initialize dummy */
dummy = PyString_FromString("<dummy key>");
if (dummy == NULL)
return NULL;
}
if (numfree) {
mp = free_list[--numfree];
assert (mp != NULL);
assert (Py_TYPE(mp) == &PyDict_Type);
_Py_NewReference((PyObject *)mp);
if (mp->ma_fill) {
EMPTY_TO_MINSIZE(mp);
} else {
/* At least set ma_table and ma_mask; these are wrong
if an empty but presized dict is added to freelist */
INIT_NONZERO_DICT_SLOTS(mp);
}
assert (mp->ma_used == 0);
assert (mp->ma_table == mp->ma_smalltable);
assert (mp->ma_mask == PyDict_MINSIZE - 1);
} else {
mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
if (mp == NULL)
return NULL;
EMPTY_TO_MINSIZE(mp);
}
mp->ma_lookup = lookdict_string;
return (PyObject *)mp;
}
步骤:
- 初始化dummy对象
- 如果缓冲池还有可用的对象,则从缓冲池中读取,否则,执行步骤3
- 分配内存空间,创建PyDictObject对象,初始化对象
- 指定添加字典元素时的探测函数,元素的搜索策略
字典搜索策略
static PyDictEntry *
lookdict(PyDictObject *mp, PyObject *key, register long hash)
{
register size_t i;
register size_t perturb;
register PyDictEntry *freeslot;
register size_t mask = (size_t)mp->ma_mask;
PyDictEntry *ep0 = mp->ma_table;
register PyDictEntry *ep;
register int cmp;
PyObject *startkey;
i = (size_t)hash & mask;
ep = &ep0[i];
if (ep->me_key == NULL || ep->me_key == key)
return ep;
if (ep->me_key == dummy)
freeslot = ep;
else {
if (ep->me_hash == hash) {
startkey = ep->me_key;
Py_INCREF(startkey);
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp < 0)
return NULL;
if (ep0 == mp->ma_table && ep->me_key == startkey) {
if (cmp > 0)
return ep;
}
else {
/* The compare did major nasty stuff to the
* dict: start over.
* XXX A clever adversary could prevent this
* XXX from terminating.
*/
return lookdict(mp, key, hash);
}
}
freeslot = NULL;
}
/* In the loop, me_key == dummy is by far (factor of 100s) the
least likely outcome, so test for that last. */
for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
i = (i << 2) + i + perturb + 1;
ep = &ep0[i & mask];
if (ep->me_key == NULL)
return freeslot == NULL ? ep : freeslot;
if (ep->me_key == key)
return ep;
if (ep->me_hash == hash && ep->me_key != dummy) {
startkey = ep->me_key;
Py_INCREF(startkey);
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp < 0)
return NULL;
if (ep0 == mp->ma_table && ep->me_key == startkey) {
if (cmp > 0)
return ep;
}
else {
/* The compare did major nasty stuff to the
* dict: start over.
* XXX A clever adversary could prevent this
* XXX from terminating.
*/
return lookdict(mp, key, hash);
}
}
else if (ep->me_key == dummy && freeslot == NULL)
freeslot = ep;
}
assert(0); /* NOT REACHED */
return 0;
}
字典在添加元素和查询元素时,都需要用到字典的搜索策略,搜索时,如果不存在该key,那么返回Unused状态的entry,如果存在该key,但是key是一个Dummy对象,那么返回Dummy状态的entry,其他情况就表示存在Active状态的entry,那么对于字典的插入操作,针对不同的情况进行操作也不一样。对于Active的entry,直接替换me_value值即可;对于Unused或Dummy的entry,需要同时设置me_key,me_hash和me_value
PYDICTOBJECT对象缓冲池
PyDictObject对象缓冲池和PyListObject对象缓冲池的原理是类似的,都是在对象被销毁的时候把该对象添加到缓冲池中去,而且值保留PyDictObject对象本身,如果ma_table维护的时从系统堆中申请的空间,那么Python会释放这块内存,如果ma_table维护的是ma_smalltable,那么只需把smalltable中的元素的引用计数减少即可。
static void
dict_dealloc(register PyDictObject *mp)
{
register PyDictEntry *ep;
Py_ssize_t fill = mp->ma_fill;
PyObject_GC_UnTrack(mp);
Py_TRASHCAN_SAFE_BEGIN(mp)
for (ep = mp->ma_table; fill > 0; ep++) {
if (ep->me_key) {
--fill;
Py_DECREF(ep->me_key);
Py_XDECREF(ep->me_value);
}
}
if (mp->ma_table != mp->ma_smalltable)
PyMem_DEL(mp->ma_table);
if (numfree < PyDict_MAXFREELIST && Py_TYPE(mp) == &PyDict_Type)
free_list[numfree++] = mp;
else
Py_TYPE(mp)->tp_free((PyObject *)mp);
Py_TRASHCAN_SAFE_END(mp)
}
dict数据使用hash表进行存储,通过把key和value映射到表中的一个位置来访问记录。如上图所示,将dict中的key和value全部映射到一块连续的内存空间中,映射的函数叫做哈希函数,存放值的数组叫做哈希表。这种查询的方式速度会非常快,更新也快。这也就是dict数据结构比list数据结构在做查询时快的原因。
通过学习上面知识,我们知道dict特点:
- dict的key必须可以hash的值(不可改变对象),也就解释了str、fronzenset、tuple数据都可以作为key来使用。
- dict的内存花销大,但是查询速度快。list查找需要遍历,而dict查找直接找hash值
- dict是无序的,添加数据或更新数据是,当hash函数计算的偏移量发生改变时,存储的数据也会发生改变