目录
Dict
字典是否是有序
在Python3.6之前,字典是无序的,但是Python3.7+,字典是有序的。
在3.6中,字典有序是一个implementation detail,在3.7才正式成为语言特性,因此3.6中无法确保100%有序。
字典的本质就是 hash 表,hash 表就是通过 key 找到其 value ,平均情况下你只需要花费 O(1) 的时间复杂度即可以完成对一个元素的查找,字典是否有序,并不是指字典能否按照键或者值进行排序,而是字典能否按照插入键值的顺序输出对应的键值。
比如,对于一个无序字典,插入顺序和遍历的顺序是不一致的:
>>> my_dict = dict()
>>> my_dict["name"] = "lowman"
>>> my_dict["age"] = 26
>>> my_dict["girl"] = "Tailand"
>>> my_dict["money"] = 80
>>> my_dict["hourse"] = None
>>> for key,value in my_dict.items():
... print(key,value)
...
money 80
girl Tailand
age 26
hourse None
而一个有序字典的输出是这样的:
name lowman
age 26
girl Tailand
money 80
hourse None
先从 Python3.6 之前说起。在 Python 3.6 之前,其数据结构如下图所示:

由于不同键的哈希值不一样,哈希表(entries)中的顺序是按照哈希值大小排序的,遍历时从前往后遍历并不能输出键值插入的顺序,其表现起来就是无序的。
此外,这种方式还有一个缺点,就是如果以稀疏的哈希表存储时,会浪费较多的内存空间,Python3.6 之后,对其进行了优化,哈希索引和真正的键值对分开存放,数据结构如下所示:

indices 指向了一列索引,entries 指向了原本的存储哈希表内容的结构。
你可以把 indices 理解成新的简化版的哈希表,entries 理解成一个数组,数组中的每个元素是原本应该存储的哈希结果:键和值。
查找或者插入一个元素的时候,根据键的哈希值结果取模 indices 的长度,就能得到对应的数组下标,再根据对应的数组下标到 entries 中获取到对应的结果,比如 hash("key2") % 8 的结果是 3,那么 indices[3] 的值是 1,这时候到 entries 中找到对应的 entries[1] 既为所求的结果:

这么做的好处是空间利用率得到了较大的提升,我们以 64 位操作系统为例,每个指针的长度为 8 字节,则原本需要 8 * 3 * 8 为 192
现在变成了 8 * 3 * 3 + 1 * 8 为 80,节省了 58% 左右的内存空间,如下图所示:

此外,由于 entries 是按照插入顺序进行插入的数组,对字典进行遍历时能按照插入顺序进行遍历,这也是为什么 Python3.6 以后的版本字典对象是有序的原因。
字典的查询、添加、删除的时间复杂度
字典的查询、添加、删除的平均时间复杂度都是O(1),相比列表与元祖,性能更优。
字典的实现原理
Python3.6之前的无序字典
字典底层是维护一张哈希表,可以把哈希表看成一个列表,哈希表中的每一个元素又存储了哈希值(hash)、键(key)、值(value)3个元素。
enteies = [
['--', '--', '--'],
[hash, key, value],
['--', '--', '--'],
['--', '--', '--'],
[hash, key, value],
]
带入具体的数值来介绍
# 给字典添加一个值,key为hello,value为word
# my_dict['hello'] = 'word'
# hash表初始如下
enteies = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
]
hash_value = hash('hello') # 假设值为 12343543
index = hash_value & ( len(enteies) - 1) # 假设index值计算后等于3
# 下面会将值存在enteies中
enteies = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[12343543, 'hello', 'word'], # index=3
['--', '--', '--'],
]
# 继续向字典中添加值
# my_dict['color'] = 'green'
hash_value = hash('color') # 假设值为 同样为12343543
index = hash_value & ( len(enteies) - 1) # 假设index值计算后同样等于3
# 下面会将值存在enteies中
enteies = [
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[12343543, 'hello', 'word'], # 由于index=3的位置已经被占用,且key不一样,所以判定为hash冲突,继续向下寻找
[12343543, 'color', 'green'], # 找到空余位置,则保存
]
enteies表是稀疏的,随着我们插入的值不同,enteies表会越来越稀疏(enteies也是一个会动态扩展长度的,每一此扩展长度,都会重新计算所有key的hash值),所以新的字典实现就随之出现。
Python3.7+后的新的实现方式
Python3.7+带入数据演示
# 给字典添加一个值,key为hello,value为word
# my_dict['hello'] = 'word'
# 假设是一个空列表,hash表初始如下
indices = [None, None, None, None, None, None]
enteies = []
hash_value = hash('hello') # 假设值为 12343543
index = hash_value & ( len(indices) - 1) # 假设index值计算后等于3
# 会找到indices的index为3的位置
indices = [None, None, None, 0, None, None]
# 此时enteies会插入第一个元素
enteies = [
[12343543, 'hello', 'word']
]
# 我们继续向字典中添加值
my_dict['haimeimei'] = 'lihua'
hash_value = hash('haimeimei') # 假设值为 34323545
index = hash_value & ( len(indices) - 1) # 假设index值计算后等于 0
# 会找到indices的index为0的位置
indices = [1, None, None, 0, None, None]
# 此时enteies会插入第一个元素
enteies = [
[12343543, 'hello', 'word'],
[34323545, 'haimeimei', 'lihua']
]
查询字典
# 下面是一个字典与字典的存储
more_dict = {'name': '张三', 'sex': '男', 'age': 10, 'birth': '2019-01-01'}
# 数据实际存储
indices = [None, 2, None, 0, None, None, 1, None, 3]
enteies = [
[34353243, 'name', '张三'],
[34354545, 'sex', '男'],
[23343199, 'age', 10],
[00956542, 'birth', '2019-01-01'],
]
print(more_dict['age']) # 当我们执行这句时
hash_value = hash('age') # 假设值为 23343199
index = hash_value & ( len(indices) - 1) # index = 1
entey_index = indices[1] # 数据在enteies的位置是2
value = enteies[entey_index] # 所以找到值为 enteies[2]
时间复杂度
字典的平均时间复杂度是O(1),因为字典是通过哈希算法来实现的,哈希算法不可避免的问题就是hash冲突,Python字典发生哈希冲突时,会向下寻找空余位置,直到找到位置。如果在计算key的hash值时,如果一直找不到空余位置,则字典的时间复杂度就变成了O(n)了。
常见的哈希冲突解决方法
1 开放寻址法(open addressing)
开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。
2 再哈希法
这个方法是按顺序规定多个哈希函数,每次查询的时候按顺序调用哈希函数,调用到第一个为空的时候返回不存在,调用到此键的时候返回其值。
3 链地址法
将所有关键字哈希值相同的记录都存在同一线性链表中,这样不需要占用其他的哈希地址,相同的哈希值在一条链表上,按顺序遍历就可以找到。
4 公共溢出区
其基本思想是:所有关键字和基本表中关键字为相同哈希值的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
List
Python内存管理中的基石
Python中所有类型创建对象时,底层都是与PyObject和PyVarObject结构体实现,一般情况下由单个元素组成对象内部会使用PyObject结构体(float)、由多个元素组成的对象内部会使用PyVarObject结构体
2个结构体
- PyObject,此结构体中包含3个元素。
- _PyObject_HEAD_EXTRA,用于构造双向链表。
- ob_refcnt,引用计数器。
- *ob_type,数据类型。
- PyVarObject,次结构体中包含4个元素(ob_base中包含3个元素)
- ob_base,PyObject结构体对象,即:包含PyObject结构体中的三个元素。
- ob_size,内部元素个数。
3个宏定义
- PyObject_HEAD,代指PyObject结构体。
- PyVarObject_HEAD,代指PyVarObject对象。
- _PyObject_HEAD_EXTRA,代指前后指针,用于构造双向队列。
空list占多大内存
print(list().__sizeof__()) # 40
list结构体
// PyVarObject
typedef struct {
// PyObject
PyObject ob_base;
// 列表长度,int类型,8字节
Py_ssize_t ob_size;
} PyVarObject;
#define PyObject_VAR_HEAD PyVarObject ob_base;
typedef _W64 int Py_ssize_t;
typedef struct _object {
_PyObject_HEAD_EXTRA
// 引用计数器int类型,8字节
Py_ssize_t ob_refcnt;
// 指针,8字节
PyTypeObject *ob_type;
} PyObject;
typedef struct {
PyObject_VAR_HEAD
// 指向列表元素的指针向量。 列表[0] 是ob_item[0],等等。
// 指针的指针,存放列表元素
// 为什么要用指针的指针呢?
// python的list并不直接保存元素,而是保存元素的指针,这也是同一个list中可以同时存放多种类型元素的根本原因。
// 指针8字节
PyObject **ob_item;
// 列表容量,int类型,8字节
Py_ssize_t allocated;
} PyListObject;
创建列表
PyObject *
PyList_New(Py_ssize_t size)
{
// 处理size < 0的情况
if (size < 0) {
// 打印错误日志
PyErr_BadInternalCall();
return NULL;
}
struct _Py_list_state *state = get_list_state();
PyListObject *op;
#ifdef Py_DEBUG
// PyList_New() must not be called after _PyList_Fini()
assert(state->numfree != -1);
#endif
if (state->numfree) {
state->numfree--;
op = state->free_list[state->numfree];
_Py_NewReference((PyObject *)op);
}
else {
op = PyObject_GC_New(PyListObject, &PyList_Type);
if (op == NULL) {
return NULL;
}
}
if (size <= 0) {
op->ob_item = NULL;
}
else {
// 给item分配内存
op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}
}
// 设置len=0
Py_SET_SIZE(op, size);
// 设置cap=0
op->allocated = size;
_PyObject_GC_TRACK(op);
// 返回列表指针
return (PyObject *) op;
}
添加元素
listType = list()
ele = "德玛西亚"
// 实际上是将内存地址放到了列表中,即存储了一个指针,固定大小为8字节
listType.append(ele)
static int
app1(PyListObject *self, PyObject *v)
{
// 获取列表len
Py_ssize_t n = PyList_GET_SIZE(self);
assert (v != NULL);
assert((size_t)n + 1 < PY_SSIZE_T_MAX);
/* list_resize
Add padding to make the allocated size multiple of 4.
The growth pattern is: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ...
*/
// 判断cap是否够,n+1因为操作是增加元素.
if (list_resize(self, n+1) < 0)
return -1;
// 追加元素
Py_INCREF(v);
PyList_SET_ITEM(self, n, v);
return 0;
}
int
PyList_Append(PyObject *op, PyObject *newitem)
{
if (PyList_Check(op) && (newitem != NULL))
return app1((PyListObject *)op, newitem);
PyErr_BadInternalCall();
return -1;
}
插入元素
static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
// 计算len
Py_ssize_t i, n = Py_SIZE(self);
PyObject **items;
if (v == NULL) {
PyErr_BadInternalCall();
return -1;
}
assert((size_t)n + 1 < PY_SSIZE_T_MAX);
// 扩容或者缩容
if (list_resize(self, n+1) < 0)
return -1;
if (where < 0) {
where += n;
if (where < 0)
where = 0;
}
if (where > n)
where = n;
// 移位
items = self->ob_item;
for (i = n; --i >= where; )
items[i+1] = items[i];
Py_INCREF(v);
items[where] = v;
return 0;
}
int
PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem)
{
if (!PyList_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
return ins1((PyListObject *)op, where, newitem);
}
删除元素
只将len
减少1,如果下次append操作,会将第三个覆盖。
static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
/*[clinic end generated code: output=6bd69dcb3f17eca8 input=b83675976f329e6f]*/
{
PyObject *v;
int status;
// if len(list) == 0 触发panic
if (Py_SIZE(self) == 0) {
/* Special-case most common failure cause */
PyErr_SetString(PyExc_IndexError, "pop from empty list");
return NULL;
}
// 如果索引小于0,index=len(list)+index
if (index < 0)
index += Py_SIZE(self);
// 索引越界
if (!valid_index(index, Py_SIZE(self))) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
// 找到val
v = self->ob_item[index];
if (index == Py_SIZE(self) - 1) {
// 计算容量扩缩容
status = list_resize(self, Py_SIZE(self) - 1);
// 直接返回
if (status >= 0)
return v; /* and v now owns the reference the list had */
else
return NULL;
}
Py_INCREF(v);
// 移动数据
status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
if (status < 0) {
Py_DECREF(v);
return NULL;
}
return v;
}
清空元素
直接将结构体元素的指针置为 NULL即可,GC将没有引用的内存地址回收。
static int
_list_clear(PyListObject *a)
{
Py_ssize_t i;
PyObject **item = a->ob_item;
if (item != NULL) {
/* Because XDECREF can recursively invoke operations on
this list, we make it empty first. */
i = Py_SIZE(a);
Py_SET_SIZE(a, 0);
// 直接将item改为NULL
a->ob_item = NULL;
a->allocated = 0;
while (--i >= 0) {
Py_XDECREF(item[i]);
}
// 释放无用内存
PyMem_Free(item);
}
/* Never fails; the return value can be ignored.
Note that there is no guarantee that the list is actually empty
at this point, because XDECREF may have populated it again! */
return 0;
}
销毁列表
- 引用计数器>0,不操作
- 否则:将每一个元素的引用计数减一,再将结构体参数改为对应的空值,将PyListObject放入freelist(容量为80)中等待销毁。
创建列表会先去freelist中找一个拿出来,就不用再次申请内存销毁内存了。
扩缩容
static int
// Py_ssize_t : len长度,要新增就传入len+1,要删除就传入len-1
// PyListObject : list指针
list_resize(PyListObject *self, Py_ssize_t newsize)
{
PyObject **items;
size_t new_allocated, num_allocated_bytes;
// allocated是cap
Py_ssize_t allocated = self->allocated;
// 如果cap大于len,并且 len >= (cap/2),表示cap足够,直接修改len,然后进行操作即可,返回状态码status=0
if (allocated >= newsize && newsize >= (allocated >> 1)) {
assert(self->ob_item != NULL || newsize == 0);
// 设置结构体的len为newsize
Py_SET_SIZE(self, newsize);
return 0;
}
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* Add padding to make the allocated size multiple of 4.
* The growth pattern is: 0, 4, 8, 16, 24, 32, 40, 52, 64, 76, ...
* Note: new_allocated won't overflow because the largest possible value
* is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
*/
// 扩容: cap < newsize
// 缩容: newsize < (cap/2)
// 扩缩容机制算法:((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;
new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;
if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize))
new_allocated = ((size_t)newsize + 3) & ~(size_t)3;
if (newsize == 0)
new_allocated = 0;
if (new_allocated <= (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {
num_allocated_bytes = new_allocated * sizeof(PyObject *);
// 基于realloc进行扩缩容,可能涉及原来元素的迁移。
items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
}
else {
// integer overflow
items = NULL;
}
if (items == NULL) {
PyErr_NoMemory();
return -1;
}
// 重新设置list结构体
self->ob_item = items;
Py_SET_SIZE(self, newsize);
self->allocated = new_allocated;
return 0;
}