Python字典和列表底层实现原理

本文解析了Python字典从无序到有序的变化过程,详细介绍了Python3.6及以前版本字典的无序特性,以及3.7版本后字典如何通过改进的数据结构实现有序。同时探讨了字典的哈希冲突解决方法及其内部实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 之前,其数据结构如下图所示:

5e54ea0e23beb493a7459b460b2dd5ae.png

由于不同键的哈希值不一样,哈希表(entries)中的顺序是按照哈希值大小排序的,遍历时从前往后遍历并不能输出键值插入的顺序,其表现起来就是无序的。

此外,这种方式还有一个缺点,就是如果以稀疏的哈希表存储时,会浪费较多的内存空间,Python3.6 之后,对其进行了优化,哈希索引和真正的键值对分开存放,数据结构如下所示:

d0087695c091264b9b2c9b681801f888.png

indices 指向了一列索引,entries 指向了原本的存储哈希表内容的结构。

你可以把 indices 理解成新的简化版的哈希表,entries 理解成一个数组,数组中的每个元素是原本应该存储的哈希结果:键和值。

查找或者插入一个元素的时候,根据键的哈希值结果取模 indices 的长度,就能得到对应的数组下标,再根据对应的数组下标到 entries 中获取到对应的结果,比如 hash("key2") % 8 的结果是 3,那么 indices[3] 的值是 1,这时候到 entries 中找到对应的 entries[1] 既为所求的结果:

ce1dd18f8814e82f4a329922c2337929.png

这么做的好处是空间利用率得到了较大的提升,我们以 64 位操作系统为例,每个指针的长度为 8 字节,则原本需要 8 * 3 * 8 为 192

现在变成了 8 * 3 * 3 + 1 * 8 为 80,节省了 58% 左右的内存空间,如下图所示:

72c22f6defb7e49f495d29f8275f4b05.png

此外,由于 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个结构体

  1. PyObject,此结构体中包含3个元素。
    1. _PyObject_HEAD_EXTRA,用于构造双向链表。
    2. ob_refcnt,引用计数器。
    3. *ob_type,数据类型。
  2. PyVarObject,次结构体中包含4个元素(ob_base中包含3个元素)
    1. ob_base,PyObject结构体对象,即:包含PyObject结构体中的三个元素。
    2. ob_size,内部元素个数。

3个宏定义

  1. PyObject_HEAD,代指PyObject结构体。
  2. PyVarObject_HEAD,代指PyVarObject对象。
  3. _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;
}

销毁列表

  1. 引用计数器>0,不操作
  2. 否则:将每一个元素的引用计数减一,再将结构体参数改为对应的空值,将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;
}
列表Python 列表底层实现是一个数组结构,数组中的每个元素都是一个指针,指向实际存储的元素对象。因为 Python 列表的长度是可变的,所以在插入或删除元素时,Python 会重新分配内存,将原有的元素拷贝到新的内存中,以实现动态扩容或缩容。 元组: Python 元组底层实现列表类似,也是一个数组结构,但是元组是不可变的,即一旦创建,就不能再改变其元素的值。因此,Python 不需要为元组提供动态扩容或缩容的支持,这使得元组在某些场景下比列表更加高效。 字典Python 字典底层实现是一个哈希表,哈希表中的每个元素都包含一个键一个值。Python 使用哈希函数将键转换为哈希值,然后使用哈希值作为索引,将值存储在哈希表中。当需要查找一个键对应的值时,Python 使用哈希函数将键转换为哈希值,然后在哈希表中查找对应的值。如果存在多个键的哈希值相同,Python 会使用链表将这些键值对连接在一起,称为哈希冲突。 集合: Python 集合底层实现是一个哈希表,类似于字典Python 集合中的每个元素都是一个键,而值则为 None。集合使用哈希表来存储键,因此集合中的元素是无序的。当需要检查一个元素是否存在于集合中时,Python 使用哈希函数将元素转换为哈希值,然后在哈希表中查找对应的键。如果键存在,则说明元素存在于集合中。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Generalzy

文章对您有帮助,倍感荣幸

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值