Python中字典的缓存池

目录
  • PyDictObject缓存池
  • PyDictKeysObject缓存池
  • 小结

前言:

我们知道字典里面有一个ma_keys和ma_values,其中ma_keys是一个指向PyDictKeysObject的指针,ma_values是一个指向PyObject *数组的二级指针。当哈希表为分离表时,键由ma_keys维护,值由ma_values维护;当哈希表为结合表时,键和值均由ma_keys维护。

那么当我们在销毁一个PyDictObject时,也肯定是要先释放ma_keys和ma_values。

如果是分离表,会将每个value的引用计数减1,然后释放ma_values;再将每个key的引用计数减1,然后释放ma_keys。最后再释放PyDictObject本身。

如果是结合表,由于key、value都在ma_keys中,将每个key、value的引用计数减1之后,只需要再释放ma_keys即可。最后再释放PyDictObject本身。

整个过程还是很清晰的,只不过这里面遗漏了点什么东西,没错,就是缓存池。在介绍浮点数的时候,我们说不同的对象都有自己的缓存池,当然字典也不例外。并且除了PyDictObject之外,PyDictKeysObject也有相应的缓存池,毕竟它负责存储具体的键值对。

那么下面我们就来研究一下这两者的缓存池。

PyDictObject缓存池

字典的缓存池和列表的缓存池高度相似,都是采用数组实现的,并且容量也是80个。

#ifndef PyDict_MAXFREELIST
#define PyDict_MAXFREELIST 80
#endif
static PyDictObject *free_list[PyDict_MAXFEELIST];
static int numfree = 0;  //缓存池当前存储的元素个数

开始时,这个缓存池什么也没有,直到第一个PyDictObject对象被销毁时,缓存池里面才开始接纳被销毁的PyDictObject对象。

static void
dict_dealloc(PyDictObject *mp)
{
    //获取ma_values指针
    PyObject **values = mp->ma_values;
    //获取ma_keys指针
    PyDictKeysObject *keys = mp->ma_keys;
    Py_ssize_t i, n;

    //因为要被销毁,所以让GC不再跟踪
    PyObject_GC_UnTrack(mp);
    //用于延迟释放
    Py_TRASHCAN_SAFE_BEGIN(mp)

    //调整引用计数
    //如果values不为NULL,说明是分离表
    if (values != NULL) {
    //将指向的value、key的引用计数减1
    //然后释放ma_values和ma_keys
        if (values != empty_values) {
            for (i = 0, n = mp->ma_keys->dk_nentries; i < n; i++) {
                Py_XDECREF(values[i]);
            }
            free_values(values);
        }
        DK_DECREF(keys);
    }
    //否则说明是结合表
    else if (keys != NULL) {
    //结合表的话,dk_refcnt一定是1
    //此时只需要释放ma_keys,因为键值对全部由它来维护
    //在DK_DECREF里面,会将每个key、value的引用计数减1
    //然后释放ma_keys
        assert(keys->dk_refcnt == 1);
        DK_DECREF(keys);
    }
    //将被销毁的对象放到缓存池当中
    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)
}

同理,当创建字典时,也会优先从缓存池里面获取。

static PyObject *
new_dict(PyDictKeysObject *keys, PyObject **values)
{
    //...
    if (numfree) {
        mp = free_list[--numfree];
    }
    //...
}

因此在缓存池的实现上,字典和列表有着很高的相似性。不仅都是由数组实现,在销毁的时候也都会放在数组的尾部,创建的时候也会从数组的尾部获取。当然啦,因为这么做符合数组的特性,如果销毁和创建都是在数组的头部操作,那么时间复杂度就从O(1)变成了O(n)。

我们用Python来测试一下:

d1 = {k: 1 for k in "abcdef"}
d2 = {k: 1 for k in "abcdef"}
print("id(d1):", id(d1))
print("id(d2):", id(d2))
# 放到缓存池的尾部
del d1
del d2
# 缓存池:[d1, d2]

# 从缓存池的尾部获取
# 显然id(d3)和上面的id(d2)是相等的
d3 = {k: 1 for k in "abcdefghijk"}
# id(d4)和上面的id(d1)是相等的
d4 = {k: 1 for k in "abcdefghijk"}
print("id(d3):", id(d3))
print("id(d4):", id(d4))
# 输出结果
"""
id(d1): 1363335780736
id(d2): 1363335780800
id(d3): 1363335780800
id(d4): 1363335780736
"""

输出结果和我们的预期是相符合的,以上就是PyDictObject的缓存池。

PyDictKeysObject缓存池

PyDictKeysObject也有自己的缓存池,同样基于数组实现,大小是80。

//PyDictObject的缓存池叫 free_list
//PyDictKeysObject的缓存池叫 keys_free_list
//两者不要搞混了
static PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST];
static int numfreekeys = 0;  //缓存池当前存储的元素个数

我们先来看看它的销毁过程:

static void
free_keys_object(PyDictKeysObject *keys)
{
    //将每个entry的me_key、me_value的引用计数减1
    for (i = 0, n = keys->dk_nentries; i < n; i++) {
        Py_XDECREF(entries[i].me_key);
        Py_XDECREF(entries[i].me_value);
    }
#if PyDict_MAXFREELIST > 0
    //将其放在缓存池当中
    //当缓存池未满、并且dk_size为8的时候被缓存
    if (keys->dk_size == PyDict_MINSIZE && numfreekeys < PyDict_MAXFREELIST) {
        keys_free_list[numfreekeys++] = keys;
        return;
    }
#endif
    PyObject_FREE(keys);
}

销毁的时候,也是放在了缓存池的尾部,那么创建的时候肯定也是先从缓存池的尾部获取。

static PyDictKeysObject *new_keys_object(Py_ssize_t size)
{
    PyDictKeysObject *dk;
    Py_ssize_t es, usable;
    //...
    //创建 ma_keys,如果缓存池有可用对象、并且size等于8,
    //那么会从 keys_free_list 中获取
    if (size == PyDict_MINSIZE && numfreekeys > 0) {
        dk = keys_free_list[--numfreekeys];
    }
    else {
        // 否则malloc重新申请
        dk = PyObject_MALLOC(sizeof(PyDictKeysObject)
                             + es * size
                             + sizeof(PyDictKeyEntry) * usable);
        }
    }
    //...
    return dk;
}

所以PyDictKeysObject的缓存池和列表同样是高度相似的,只不过它想要被缓存,还需要满足一个额外的条件,那就是dk_size必须等于8。很明显,这个限制是出于对内存方面的考量。

我们还是来验证一下:

import ctypes
class PyObject(ctypes.Structure):
    _fields_ = [("ob_refcnt", ctypes.c_ssize_t),
                ("ob_type", ctypes.c_void_p)]
class PyDictObject(PyObject):
    _fields_ = [("ma_used", ctypes.c_ssize_t),
                ("ma_version_tag", ctypes.c_uint64),
                ("ma_keys", ctypes.c_void_p),
                ("ma_values", ctypes.c_void_p)]
d1 = {_: 1 for _ in "mnuvwxyz12345"}
print(
    PyDictObject.from_address(id(d1)).ma_keys
)  # 1962690551536
# 键值对个数超过了8,dk_size必然也超过了 8
# 那么当销毁d1的时候,d1.ma_keys不会被缓存
# 而是会直接释放掉
del d1
d2 = {_: 1 for _ in "a"}
print(
    PyDictObject.from_address(id(d2)).ma_keys
)  # 1962387670624

# d2 的 dk_size 显然等于 8
# 因此它的 ma_keys 是会被缓存的
del d2
d3 = {_: 1 for _ in "abcdefg"}
print(
    PyDictObject.from_address(id(d3)).ma_keys
)  # 1962699215808
# 尽管 d2 的 ma_keys 被缓存起来了
# 但是 d3 的 dk_size 大于 8
# 因此它不会从缓存池中获取,而是重新创建
# d4 的 dk_size 等于 8
# 因此它会获取 d2 被销毁的 ma_keys
d4 = {_: 1 for _ in "abc"}
print(
    PyDictObject.from_address(id(d4)).ma_keys
)  # 1962387670624

所以从打印的结果来看,由于d4.ma_keys和d2.ma_keys是相同的,因此证实了我们的结论。不像列表和字典,它们是只要被销毁,就会放到缓存池里面,因为它们没有存储具体的数据,大小是固定的。

但是PyDictKeysObject不同,它存储了entry,每个entry占24字节。如果内部的entry非常多,那么缓存起来会有额外的内存开销。因此Python的策略是,只有在dk_size等于8的时候,才会缓存。当然这三者在缓存池的实现上,是基本一致的。

小结

总的来说,Python的字典是一个被高度优化的数据结构,因为解释器在运行的时候也重度依赖字典,这就决定了它的效率会非常高。当然,我们没有涉及字典的全部内容,比如字典有很多方法,比如keys、values、items方法等等,我们并没有说。这些有兴趣的话,可以对着源码看一遍,不是很难。总之我们平时,也可以尽量多使用字典。

到此这篇关于Python中字典的缓存池的文章就介绍到这了,更多相关Python缓存池内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 使用Python获取字典键对应值的两种方法

    目录 当知道字典的键时: 当不知道字典的键时: 附:字典dic最大值对应的键 总结 有两种方法 当知道字典的键时: unit_rooms={ 3:{301:[1,80],302:[1,80],303:[2,90],304:[2,90]}, 4:{401:[1,80],402:[1,80],403:[2,90],404:[2,90]}, 5:{501:[1,80],502:[1,80],503:[2,90],504:[2,90]} } for i in range(3,6): rooms=unit

  • 利用For循环遍历Python字典的三种方法实例

    目录 前言 方法 1:使用 For 循环 + 索引进行迭代 方法 2:使用 .keys( ) + 索引进行迭代 方法 3:使用 .items( ) 进行迭代 进阶:遍历嵌套字典 总结 前言 在Python中,如何使用“for”循环遍历字典? 今天我们将会演示三种方法,并学会遍历嵌套字典. 在实战前,我们需要先创建一个模拟数据的字典. dict_1 = {'Name': 'Zara', 'Age': 7, 'Class': 'First','Address':'Beijing'} 方法 1:使用

  • 在Python中如何让字典保持有序

    目录 1.如何让字典保持有序 2.代码演示 (1)OrderedDict有序字典简单使用 (2)模拟编写竞赛答题 1.如何让字典保持有序 实际案例: 某编程竞赛系统,对参赛选手编程解题进行计时,选手完成题目后,把该选手解题用时记录到字典中,以便赛后按选手名查询成绩.(答题用时越短,成绩越优秀.) 如:LiLei第2名用时43分钟,HanMeimei第5名用时52分钟,Jim第1名用时39分钟 {'LiLei': (2, 43), 'HanMeimei': (5, 52), 'Jim': (1,

  • python如何生成密码字典

    目录 一.密码字典 二.字典生成 1.生成6位数小写字母+数字密码字典 2.选择模式运行 一.密码字典 所谓密码字典,主要是配合解密使用,一般情况用来暴力破解密码,是由指定字符排列组合组成的文本文件.如果知道密码设置的规律指定性生成密码,会对破解密码有决定性的帮助!! 二.字典生成 1.生成6位数小写字母+数字密码字典 代码如下(示例): import itertools as its words = 'abcdefghijklmnopqrstuvwxyz1234567890'  #采用的字符

  • Python中字典常用操作的示例详解

    目录 前言 初始化 合并字典 字典推导式 Collections 标准库 字典转 JSON 字典转 Pandas 前言 字典是Python必用且常用的数据结构,本文梳理常用的字典操作,看这个就够了,涉及: 初始化 合并字典 字典推导式 Collections 标准库 字典转JSON 字典转Pandas 初始化 # 最常用这种 my_object = { "a": 5, "b": 6 } # 如果你不喜欢写大括号和双引号: my_object = dict(a=5,

  • 详解Python实现字典合并的四种方法

    目录 1.用for循环把一个字典合并到另一个字典 2.用dict(b, **a)方法构造一个新字典 3.用b.update(a)的方法,更新字典 4.把字典转换成列表合并后,再转换成字典 (1)利用a.items().b.items()把a.b两个字典转换成元组键值对列表 (2)合并列表并且把合并后的列表转换成字典 5.实例,netmiko使用json格式的数据进行自动化操作 (1)json格式的处理 (2)json格式的设备信息列表 (3)netmiko读取json类型信息示例 1.用for循

  • Python中字典的缓存池

    目录 PyDictObject缓存池 PyDictKeysObject缓存池 小结 前言: 我们知道字典里面有一个ma_keys和ma_values,其中ma_keys是一个指向PyDictKeysObject的指针,ma_values是一个指向PyObject *数组的二级指针.当哈希表为分离表时,键由ma_keys维护,值由ma_values维护:当哈希表为结合表时,键和值均由ma_keys维护. 那么当我们在销毁一个PyDictObject时,也肯定是要先释放ma_keys和ma_valu

  • Python 中创建 PostgreSQL 数据库连接池

    目录 习惯于使用数据库之前都必须创建一个连接池,即使是单线程的应用,只要有多个方法中需用到数据库连接,建立一两个连接的也会考虑先池化他们.连接池的好处多多, 1) 如果反复创建连接相当耗时, 2) 对于单个连接一路用到底的应用,有连接池时避免了数据库连接对象传来传去, 3) 忘记关连接了,连接池幸许还能帮忙在一定时长后关掉,当然密集取连接的应用势将耗尽连接, 4) 一个应用打开连接的数量是可控的 接触到 Python 后,在使用 PostgreSQL 也自然而然的考虑创建连接池,使用时从池中取,

  • Python中字典和集合学习小结

    映射类型: 表示一个任意对象的集合,且可以通过另一个几乎是任意键值的集合进行索引 与序列不同,映射是无序的,通过键进行索引 任何不可变对象都可用作字典的键,如字符串.数字.元组等 包含可变对象的列表.字典和元组不能用作键 引用不存在的键会引发KeyError异常 1)字典 dict { } 空字典 { key1:value1,key2:value2,... } 字典在其它编程语言中又称作关联数组或散列表: 通过键实现元素存取:无序集合:可变类型容器,长度可变,异构,嵌套 支持的操作: len(D

  • python中字典(Dictionary)用法实例详解

    本文实例讲述了python中字典(Dictionary)用法.分享给大家供大家参考.具体分析如下: 字典(Dictionary)是一种映射结构的数据类型,由无序的"键-值对"组成.字典的键必须是不可改变的类型,如:字符串,数字,tuple:值可以为任何python数据类型. 1.新建字典 >>> dict1={} #建立一个空字典 >>> type(dict1) <type 'dict'> 2.增加字典元素:两种方法 >>&g

  • Python中字典(dict)合并的四种方法总结

    本文主要给大家介绍了关于Python中字典(dict)合并的四种方法,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍: 字典是Python语言中唯一的映射类型. 映射类型对象里哈希值(键,key)和指向的对象(值,value)是一对多的的关系,通常被认为是可变的哈希表. 字典对象是可变的,它是一个容器类型,能存储任意个数的Python对象,其中也可包括其他容器类型. 字典类型与序列类型的区别: 1. 存取和访问数据的方式不同. 2. 序列类型只用数字类型的键(从序列的开始按数值顺序索引

  • python中字典dict常用操作方法实例总结

    本文实例总结了python中字典dict常用操作方法.分享给大家供大家参考.具体如下: 下面的python代码展示python中字典的常用操作,字典在python开发中有着举足轻重的地位,掌握字典操作相当重要 #创建一空字典 x = {} #创建包含三个项目的字典 x = {"one":1, "two":2, "three":3} #访问其中的一个元素 x['two'] #返回字典中的所有键列表 x.keys() #返回字典中的所有值列表 x.v

  • 详解python中字典的循环遍历的两种方式

    开发中经常会用到对于字典.列表等数据的循环遍历,但是python中对于字典的遍历对于很多初学者来讲非常陌生,今天就来讲一下python中字典的循环遍历的两种方式. 注意: python2和python3中,下面两种方法都是通用的. 1. 只对键的遍历 一个简单的for语句就能循环字典的所有键,就像处理序列一样: d = {'name1' : 'pythontab', 'name2' : '.', 'name3' : 'com'} for key in d: print (key, ' value

  • Python中字典的浅拷贝与深拷贝用法实例分析

    本文实例讲述了Python中字典的浅拷贝与深拷贝用法.分享给大家供大家参考,具体如下: 最近发现的一个很值得记录的东西就是python字典的浅拷贝问题 首先,明确一下什么是浅拷贝,什么是深拷贝: 简单的来说就是,在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存,使这个增加的指针指向这个新的内存 也就是说,在浅拷贝情况下,不同引用指向的是同一块内存,改其中一个引用,那么其他引用也会跟着改变 应用到python 的字典复制过程: # codi

  • 对python中字典keys,values,items的使用详解

    在python中对字典进行遍历时,可以直接使用如下模式: dict = {"name": "jack", "age": 15, "height": 1.75} for k in dict.keys(): print(k) 使用keys方法遍历得到的是key,可以依次输出,但是当单独使用dict.keys() 时,得到的结果时dict.keys类,属于迭代器,此时并不能使用列表的下标,需要转换一下,方法如下: 直接使用list(

  • 浅谈python中字典append 到list 后值的改变问题

    看一个例子 d={'test':1} d_test=d d_test['test']=2 print d 如果你在命令行实践的话,会发现你改动的是d_test ,但是d 也跟着改变了. 通常这和我们期待的不一样. Why? 因为字典d 是一个object ,而d_test=d并没有真正的将该字典在内存中再次创建.只是指向了相同的object.这也是python 提高性能,优化内存的考虑. 实际场景 d={"name":""} l=[] for i in xrange

随机推荐