`
lantian_123
  • 浏览: 1360194 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Python字典对象实现原理

阅读更多

原文链接:http://foofish.net/blog/92/python_dict_implements

字典类型是Python中最常用的数据类型之一,它是一个键值对的集合,字典通过键来索引,关联到相对的值,理论上它的查询复杂度是 O(1) :

>>> d = {'a': 1, 'b': 2}
>>> d['c'] = 3
>>> d
{'a': 1, 'b': 2, 'c': 3}

字符串的实现原理文章中,曾经出现过字典对象用于intern操作,那么字典的内部结构是怎样的呢?PyDictObject对象就是dict的内部实现。

哈希表 (hash tables)

哈希表(也叫散列表),根据关键值对(Key-value)而直接进行访问的数据结构。它通过把key和value映射到表中一个位置来访问记录,这种查询速度非常快,更新也快。而这个映射函数叫做哈希函数,存放值的数组叫做哈希表。 哈希函数的实现方式决定了哈希表的搜索效率。具体操作过程是:

  1. 数据添加:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
  2. 数据查询:再次使用哈希函数将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有三种状态。

  1. Unused: me_key == me_value == NULL

    Unused是entry的初始状态,key和value都为NULL。插入元素时,Unused状态转换成Active状态。这是me_key为NULL的唯一情况。

  2. Active: me_key != NULL and me_key != dummy 且 me_value != NULL

    插入元素后,entry就成了Active状态,这是me_value唯一不为NULL的情况,删除元素时Active状态刻转换成Dummy状态。

  3. 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是一种类似的伪删除方式,保证探测链的连续性。
python_entry_status

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;
}

  1. 初始化dummy对象
  2. 如果缓冲池还有可用的对象,则从缓冲池中读取,否则,执行步骤3
  3. 分配内存空间,创建PyDictObject对象,初始化对象
  4. 指定添加字典元素时的探测函数,元素的搜索策略

字典搜索策略

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)
}
2
3
分享到:
评论

相关推荐

    Python字典对象实现原理详解

    主要介绍了Python字典对象实现原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    049.Python字典_核心底层原理_内存分析_查找值对象过程.mp4

    049.Python字典_核心底层原理_内存分析_查找值对象过程.mp4

    Python字典的核心底层原理讲解

    字典对象的核心是散列表。散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做 bucket。每个 bucket 有两部分:一个是键对象的引用,一个是值对象的引用。所有 bucket 结构和大小一致,我们可以通过...

    数据结构概念、栈、队列、链表与数组、字典与对象实现原理(详细的代码)

    python中字典对象实现原理 数组 一. 数据结构中的一些概念 1、数据结构是什么 简单来说,数据结果就是设计数据以何种方式存储在计算机中 比如:列表,集合,与字典等都是一种数据结构 程序 = 数据结构 + 算法 2、...

    Python3 shelve对象持久存储原理详解

    不需要关系数据库时,可以用shelve模块作为持久存储Python对象的一个简单的选择。类似于字典,shelf按键访问。值将被pickled并写至由dbm创建和管理的数据库。 1.1 创建一个新shelf 使用shelve最简单的方法就是利用...

    2019千峰Python超详细入门教程(百度云盘分享).docx

    ├─千锋Python教程:第05章 字典&集合&类型转换&turtle;(1集) │ │ .DS_Store │ │ │ ├─code │ │ 1、dict(字典).py │ │ 2、set.py │ │ 3、类型转换.py │ │ │ └─video │ 千锋Python教程:32....

    python学习课件+python源码90个合集.7z

    046魔法方法:描述符(Property的原理)(课件 源代码) 047魔法方法:定制序列(课件 源代码) 048魔法方法:迭代器(课件 源代码) 049乱入:生成器(课件) 050模块:模块就是程序(课件 源代码) 051模块:__...

    Python命名空间及作用域原理实例解析

    Python命名空间和作用域 总结 emmm,这一块讲了2个内容,一个是命名空间,一个是作用域。一个一个说吧 ...python的命名空间由python数据结构字典实现。 python的命名空间细分的话有三种。如图所示。 这

    Python学习资料学习课件python基础源码.zip

    046魔法方法:描述符(Property的原理) 047魔法方法:定制序列 048魔法方法:迭代器 049乱入:生成器 050模块:模块就是程序 051模块:__name__='__main__'、搜索路径和包 052模块:像个极客一样去思考 053论一只...

    Python最全学习大纲-玩转Python

    通过本资源,您将学习到 Python 中的各种数据结构如列表、字典、集合等的使用方法,掌握函数编程和面向对象编程的基本原理和实践技巧。您还将了解异常处理的重要性以及如何进行文件操作和读写操作。通过这些内容的...

    python基础学习资料+配套题目+答案详解

    计算机组成原理和Python基础语法知识;2.判断语句和循环语句;3.容器:字符串、列表、元组、字典;4.容器:元组、字典;5.函数(一);6:函数(二);7.文件的操作、综合应用;8.面向对象基础(一);9.面向对象...

    Python数据处理单元四 使用pandas进行数据分组与聚合.docx

    分组聚合的原理一般分为拆分-应用-合并。( ) 只要使用groupby()方法分组就会产生一个DataFrameGroupby对象。( ) 使用agg()方法进行聚合运算会对产生的标量值进行广播。( ) 使用transform()方法进行聚合运算,...

    python从入门到精通地址.txt

    046魔法方法:描述符(Property的原理) P48. 047魔法方法:定制序列 P49. 048魔法方法:迭代器 P50. 049乱入:生成器 P51. 050模块:模块就是程序 P52. 051模块:__name__=___main___、搜索路径和包 P53. ...

    python入门到高级全栈工程师培训 第3期 附课件代码

    04 Python 字典的魔法 05 Python 错误更正:布尔值可以作为字典的key 06 Python 今日内容整理 第13章 第13章共1课 第14章 01 数据类型和变量总结 02 集合定义和基本操作方法 03 集合关系运算交,差,并集 04 ...

    你的Python入门好帮手:一份包含了Python基础学习需要的知识框架 + 爬虫基础 + numpy基础

    Python是一种多范式编程语言,既适合面向对象编程,也适合函数式编程和过程式编程。它语法简洁明了,易于上手,因此成为许多人入门编程的首选语言。以下是对Python入门的一些补充说明: 1. Python基础知识 - 变量、数据...

    Python命名空间namespace及作用域原理解析

    主要是通过字典来实现的。 在python中,函数、模块等都有自己的命名空间: 局部命名空间(local namespace):即函数中定义的名称 —— 包括函数中的变量、参数、局部变量等; 全局命名空间(global namespace):即...

    Python filter过滤器原理及实例应用

    其实filter就是一个“过滤器”:把【可迭代的变量】中的值,挨个地传给函数进行处理,那些使得函数的返回值为True的变量组成的迭代器对象就是filter表达式的结果 那filter的第一个参数,即函数的返回的值必须是bool...

    从列表或字典创建Pandas的DataFrame对象的方法

    在这些情况下,了解如何从标准python列表或字典创建DataFrames会很有帮助。 基本过程并不困难,但因为有几种不同的选择,所以有助于理解每种方法的工作原理。 我永远记不住我是否应该使用 from_dict , from_...

    minpiler:将 python 代码编译为 Mindustry 微处理器指令

    将此视为重用 Python 语法而不是 Python 实现的努力。 警告:所有需要动态内存的东西都不起作用:任何数据结构(列表、字典等)、类、闭包、递归。 此外,您不能使用任何 Python 内置函数(输入、评估、异常等)。 ...

Global site tag (gtag.js) - Google Analytics