Python dictionary implementation

http://www.laurentluce.com/posts/python-dictionary-implementation/

August 29, 2011

This post describes how dictionaries are implemented in the Python language.

Dictionaries are indexed by keys and they can be seen as associative arrays. Let’s add 3 key/value pairs to a dictionary:

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

The values can be accessed this way:

01 >>> d['a']
02 1
03 >>> d['b']
04 2
05 >>> d['c']
06 3
07 >>> d['d']
08 Traceback (most recent call last):
09   File "<stdin>", line 1, in <module>
10 KeyError: 'd'

The key ‘d’ does not exist so a KeyError exception is raised.

Hash tables

Python dictionaries are implemented using hash tables. It is an array whose indexes are obtained using a hash function on the keys. The goal of a hash function is to distribute the keys evenly in the array. A good hash function minimizes the number of collisions e.g. different keys having the same hash. Python does not have this kind of hash function. Its most important hash functions (for strings and ints) are very regular in common cases:

1 >>> map(hash, (0, 1, 2, 3))
2 [0, 1, 2, 3]
3 >>> map(hash, ("namea""nameb""namec""named"))
4 [-1658398457, -1658398460, -1658398459, -1658398462]

We are going to assume that we are using strings as keys for the rest of this post. The hash function for strings in Python is defined as:

01 arguments: string object
02 returns: hash
03 function string_hash:
04     if hash cached:
05         return it
06     set len to string's length
07     initialize var p pointing to 1st char of string object
08     set x to value pointed by p left shifted by 7 bits
09     while len >= 0:
10         set var x to (1000003 * x) xor value pointed by p
11         increment pointer p
12     set x to x xor length of string object
13     cache x as the hash so we don't need to calculate it again
14     return x as the hash

If you run hash(‘a’) in Python, it will execute string_hash() and return 12416037344. Here we assume we are using a 64-bit machine.

If an array of size x is used to store the key/value pairs then we use a mask equal to x-1 to calculate the slot index of the pair in the array. This makes the computation of the slot index fast. The probability to find an empty slot is high due to the resizing mechanism described below. This means that having a simple computation makes sense in most of the cases. If the size of the array is 8, the index for ‘a’ will be: hash(‘a’) & 7 = 0. The index for ‘b’ is 3, the index for ‘c’ is 2, the index for ‘z’ is 3 which is the same as ‘b’, here we have a collision.

We can see that the Python hash function does a good job when the keys are consecutive which is good because it is quite common to have this type of data to work with. However, once we add the key ‘z’, there is a collision because it is not consecutive enough.

We could use a linked list to store the pairs having the same hash but it would increase the lookup time e.g. not O(1) average anymore. The next section describes the collision resolution method used in the case of Python dictionaries.

Open addressing

Open addressing is a method of collision resolution where probing is used. In case of ‘z’, the slot index 3 is already used in the array so we need to probe for a different index to find one which is not already used. Adding a key/value pair will average O(1) and the lookup operation too.

A quadratic probing sequence is used to find a free slot. The code is the following:

1 j = (5*j) + 1 + perturb;
2 perturb >>= PERTURB_SHIFT;
3 use j % 2**i as the next table index;

Recurring on 5*j+1 quickly magnifies small differences in the bits that didn’t affect the initial index. The variable “perturb” gets the other bits of the hash code into play.

Just out of curiosity, let’s look at the probing sequence when the table size is 32 and j = 3.
3 -> 11 -> 19 -> 29 -> 5 -> 6 -> 16 -> 31 -> 28 -> 13 -> 2…

You can read more about this probing sequence by looking at the source code of dictobject.c. A detailed explanation of the probing mechanism can be found at the top of the file.

Now, let’s look at the Python internal code along with an example.

Dictionary C structures

The following C structure is used to store a dictionary entry: key/value pair. The hash, key and value are stored. PyObject is the base class of the Python objects.

1 typedef struct {
2     Py_ssize_t me_hash;
3     PyObject *me_key;
4     PyObject *me_value;
5 } PyDictEntry;

The following structure represents a dictionary. ma_fill is the number of used slots + dummy slots. A slot is marked dummy when a key pair is removed. ma_used is the number of used slots (active). ma_mask is equal to the array’s size minus 1 and is used to calculate the slot index. ma_table is the array and ma_smalltable is the initial array of size 8.

01 typedef struct _dictobject PyDictObject;
02 struct _dictobject {
03     PyObject_HEAD
04     Py_ssize_t ma_fill;
05     Py_ssize_t ma_used;
06     Py_ssize_t ma_mask;
07     PyDictEntry *ma_table;
08     PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
09     PyDictEntry ma_smalltable[PyDict_MINSIZE];
10 };

Dictionary initialization

When you first create a dictionary, the function PyDict_New() is called. I removed some of the lines and converted the C code to pseudocode to concentrate on the key concepts.

1 returns new dictionary object
2 function PyDict_New:
3     allocate new dictionary object
4     clear dictionary's table
5     set dictionary's number of used slots + dummy slots (ma_fill) to 0
6     set dictionary's number of active slots (ma_used) to 0
7     set dictionary's mask (ma_value) to dictionary size - 1 = 7
8     set dictionary's lookup function to lookdict_string
9     return allocated dictionary object

Adding items

When a new key/value pair is added, PyDict_SetItem() is called. This function takes a pointer to the dictionary object and the key/value pair. It checks if the key is a string and calculates the hash or reuses the one cached if it exists. insertdict() is called to add the new key/value pair and the dictionary is resized if the number of used slots + dummy slots is greater than 2/3 of the array’s size.
Why 2/3? It is to make sure the probing sequence can find a free slot fast enough. We will look at the resizing function later.

01 arguments: dictionary, key, value
02 returns: 0 if OK or -1
03 function PyDict_SetItem:
04     if key's hash cached:
05         use hash
06     else:
07         calculate hash
08     call insertdict with dictionary object, key, hash and value
09     if key/value pair added successfully and capacity over 2/3:
10         call dictresize to resize dictionary's table

inserdict() uses the lookup function lookdict_string() to find a free slot. This is the same function used to find a key. lookdict_string() calculates the slot index using the hash and the mask values. If it cannot find the key in the slot index = hash & mask, it starts probing using the loop described above, until it finds a free slot. At the first probing try, if the key is null, it returns the dummy slot if found during the first lookup. This gives priority to re-use the previously deleted slots.

We want to add the following key/value pairs: {‘a’: 1, ‘b’: 2′, ‘z’: 26, ‘y’: 25, ‘c’: 5, ‘x’: 24}. This is what happens:

A dictionary structure is allocated with internal table size of 8.

  • PyDict_SetItem: key = ‘a’, value = 1
    • hash = hash(‘a’) = 12416037344
    • insertdict
      • lookdict_string
        • slot index = hash & mask = 12416037344 & 7 = 0
        • slot 0 is not used so return it
      • init entry at index 0 with key, value and hash
      • ma_used = 1, ma_fill = 1
  • PyDict_SetItem: key = ‘b’, value = 2
    • hash = hash(‘b’) = 12544037731
    • insertdict
      • lookdict_string
        • slot index = hash & mask = 12544037731 & 7 = 3
        • slot 3 is not used so return it
      • init entry at index 3 with key, value and hash
      • ma_used = 2, ma_fill = 2
  • PyDict_SetItem: key = ‘z’, value = 26
    • hash = hash(‘z’) = 15616046971
    • insertdict
      • lookdict_string
        • slot index = hash & mask = 15616046971 & 7 = 3
        • slot 3 is used so probe for a different slot: 5 is free
      • init entry at index 5 with key, value and hash
      • ma_used = 3, ma_fill = 3
  • PyDict_SetItem: key = ‘y’, value = 25
    • hash = hash(‘y’) = 15488046584
    • insertdict
      • lookdict_string
        • slot index = hash & mask = 15488046584 & 7 = 0
        • slot 0 is used so probe for a different slot: 1 is free
      • init entry at index 1 with key, value and hash
      • ma_used = 4, ma_fill = 4
  • PyDict_SetItem: key = ‘c’, value = 3
    • hash = hash(‘c’) = 12672038114
    • insertdict
      • lookdict_string
        • slot index = hash & mask = 12672038114 & 7 = 2
        • slot 2 is free so return it
      • init entry at index 2 with key, value and hash
      • ma_used = 5, ma_fill = 5
  • PyDict_SetItem: key = ‘x’, value = 24
    • hash = hash(‘x’) = 15360046201
    • insertdict
      • lookdict_string
        • slot index = hash & mask = 15360046201 & 7 = 1
        • slot 1 is used so probe for a different slot: 7 is free
      • init entry at index 7 with key, value and hash
      • ma_used = 6, ma_fill = 6

This is what we have so far:

6 slots on 8 are used now so we are over 2/3 of the array’s capacity. dictresize() is called to allocate a larger array. This function also takes care of copying the old table entries to the new table.

dictresize() is called with minused = 24 in our case which is 4 * ma_used. 2 * ma_used is used when the number of used slots is very large (greater than 50000). Why 4 times the number of used slots? It reduces the number of resize steps and it increases sparseness.

The new table size needs to be greater than 24 and it is calculated by shifting the current size 1 bit left until it is greater than 24. It ends up being 32 e.g. 8 -> 16 -> 32.

This is what happens with our table during resizing: a new table of size 32 is allocated. Old table entries are inserted into the new table using the new mask value which is 31. We end up with the following:

Removing items

PyDict_DelItem() is called to remove an entry. The hash for this key is calculated and the lookup function is called to return the entry. The slot is now a dummy slot.

We want to remove the key ‘c’ from our dictionary. We end up with the following array:

Python dictionary implementation的更多相关文章

  1. Python dictionary 字典 常用法

    Python dictionary 字典 常用法 d = {} d.has_key(key_in)       # if has the key of key_in d.keys()          ...

  2. PythonStudy——Python字典底层实现原理 The underlying implementation principle of Python dictionary

    在Python中,字典是通过散列表或说哈希表实现的.字典也被称为关联数组,还称为哈希数组等.也就是说,字典也是一个数组,但数组的索引是键经过哈希函数处理后得到的散列值.哈希函数的目的是使键均匀地分布在 ...

  3. sort a Python dictionary by value

    首先要明确一点,Python的dict本身是不能被sort的,更明确地表达应该是"将一个dict通过操作转化为value有序的列表" 有以下几种方法: 1. import oper ...

  4. python : dictionary changed size during iteration

    1. 错误方式 #这里初始化一个dict >>> d = {'a':1, 'b':0, 'c':1, 'd':0} #本意是遍历dict,发现元素的值是0的话,就删掉 >> ...

  5. python dictionary的遍历

    d = {'x':1, 'y':3, 'z':2} for k in d:    print d[k] 直接遍历k in d的话,遍历的是dictionary的keys. 2 字典的键可以是任何不可变 ...

  6. Python dictionary 字典

    Python字典是另一种可变容器模型,且可存储任意类型对象,如字符串.数字.元组等其他容器模型. 一.创建字典字典由键和对应值成对组成.字典也被称作关联数组或哈希表.基本语法如下: dict = {' ...

  7. How to create a Python dictionary with double quotes as default quote format?

    couples = [ ['jack', 'ilena'], ['arun', 'maya'], ['hari', 'aradhana'], ['bill', 'samantha']] pairs = ...

  8. Python:dictionary

    # Python3.4 Eclipse+PyDev 打开Eclipse,找到Help菜单栏,进入Install New Software…选项. # 点击work with:输入框的旁边点击Add…, ...

  9. Think Python - Chapter 11 - Dictionaries

    Dictionaries A dictionary is like a list, but more general. In a list, the indices have to be intege ...

随机推荐

  1. IOS 弹出式 POPMenuView

    //MenuView.h   // //  MenuView.h //  RockPopMenu // //  Created by zhuang chaoxiao on 14-6-26. //  C ...

  2. hadoop1.2.1三种模式配置

    1.本地模式 本地模式的安装 将hadoop 安装包解压后不用任何配置就是默认的本地模式 此时 core-site.xml ,hdfs-site.xml , marped.site.xml 三个配置文 ...

  3. CSS:7个你可能不认识的单位

    原文:7 CSS Units You Might Not Know About 众所周知,当使用CSS技术的时候,很容被一些奇异问题给困住.而当我们面对新的问题时,这会让我们处于非常不利的位置. 但是 ...

  4. C语言的代码内存布局

    由以下3个部分组成: 1)BSS 段 BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域.BSS是英文Block Started by Symbol的简称.BSS ...

  5. 修改 myeclipse8.5 servlet 模板

    在myeclipse8.5的安装目录下找到 \Common\plugins下的com.genuitec.eclipse.wizards_8.5.0.zmyeclipse75020090612.jar ...

  6. MFC使用ShowWindow(SW_MAXIMIZE)任务栏消失的处理

    ShowWindow(SW_SHOWMAXIMIZED);//窗口最大化 问题:在写程序时,如果包含了标题栏,但是没有包含最大化按钮或者最小话按钮. 那么人工用ShowWindow(SW_MAXIMI ...

  7. 题目1433:FatMouse (未解决)

    题目描述: FatMouse prepared M pounds of cat food, ready to trade with the cats guarding the warehouse co ...

  8. 使用SignalR实现比特币价格实时刷新

    ASP.NET SignalR是微软支持的一个运行在 Dot NET 平台上的 HTML Websocket 框架.它出现的主要目的是实现服务器主动推送(Push)消息到客户端页面,这样客户端就不必重 ...

  9. 开元硬件平台 Arduino

    开放源代码的电路图设计,程序开发接口免费下载,也可依个人需求自己修改. Arduino不仅仅是全球最流行的开源硬件,也是一个优秀的硬件开发平台,更是硬件开发的趋势.Arduino简单的开发方式使得开发 ...

  10. SDK "iphoneos" cannot be located

    在MAC下,交叉编译libvlc出现的一些问题和解决方法.项目中使用了libvlc开源库.在执行编译脚本中,遇到一句xcrun --sdk iphoneos --show-sdk-path报错 mac ...