This page looks best with JavaScript enabled

Python2 Source Code Study: Dictionary and List Implementation

 ·  ☕ 5 min read

1. Why Dictionary Lookups Are Faster Than List Lookups

First, take a look at the following piece of code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from time import time

t = time()
data = [chr(i) for i in range(97, 123)]
# data = dict.fromkeys(data,True)
print data
for i in range(9999999):
    after_filter = []
    for find in ['aa', 'b', 'cc', 'd', 'ee']:
        if find not in data:
            after_filter.append(find)
print after_filter
print time() - t

Run it as is:

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
['aa', 'cc', 'ee']
24.5699999332

After removing the comment on data = dict.fromkeys(data,True):

{'a': True, 'c': True, 'b': True, 'e': True, 'd': True, 'g': True, 'f': True, 'i': True, 'h': True, 'k': True, 'j': True, 'm': True, 'l': True, 'o': True, 'n': True, 'q': True, 'p': True, 's': True, 'r': True, 'u': True, 't': True, 'w': True, 'v': True, 'y': True, 'x': True, 'z': True}
['aa', 'cc', 'ee']
17.8080000877

Repeated many times, the result does not vary by more than 1 second. Why does converting the list into a dictionary with the dict.fromkeys function make it run noticeably faster?

A Python dictionary uses a hash table, so the complexity of a lookup is O(1), while a list is really an array; searching a list requires traversing the whole list, with complexity O(n). So for operations such as member lookup and access, a dictionary is faster than a list. Below, let us look together at how the list and dictionary structures are implemented in the Python source code.

2. Common Objects in the Python Source Code

All objects in Python inherit from PyObject or PyVarObject. Objects that inherit from PyObject have a fixed length, such as int. Objects that inherit from PyVarObject have a variable length, such as list and dict. PyObject and PyVarObject share the same header, PyObject_HEAD.

  • PyObject_HEAD

Defined in include/object.h

1
2
3
4
#define PyObject_HEAD                   \
    _PyObject_HEAD_EXTRA                \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

_PyObject_HEAD_EXTRA is a doubly linked list structure used for garbage collection. ob_refcnt is the object’s reference count; when there are no references to it, the memory is reclaimed automatically. ob_type is a pointer to the type object and determines the object’s type.

  • PyObject

Defined in include/object.h

1
2
3
typedef struct _object {
    PyObject_HEAD
} PyObject;
  • PyVarObject

Defined in include/object.h

1
2
3
4
5
6
7
#define PyObject_VAR_HEAD               \
    PyObject_HEAD                       \
    Py_ssize_t ob_size; // 可变部分的元素数量

typedef struct {
    PyObject_VAR_HEAD
} PyVarObject;

PyVarObject contains a set of objects, whose count is given by ob_size.

3. How Lists Are Implemented in Python

  • Definition

include/listobject.h

1
2
3
4
5
6
7
#define PyList_MAXFREELIST 80

typedef struct {
    PyObject_VAR_HEAD   // 表示变长对象,其中的 ob_size 表示实际使用的内存大小
    PyObject **ob_item; // 列表元素所在内存块的首地址
    Py_ssize_t allocated; // 列表总共分配的内存数量
} PyListObject;
  • Built-in functions

Objects/listobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static PyMethodDef list_methods[] = {
    {"__getitem__", (PyCFunction)list_subscript, METH_O|METH_COEXIST, getitem_doc},
    {"__reversed__",(PyCFunction)list_reversed, METH_NOARGS, reversed_doc},
    {"__sizeof__",  (PyCFunction)list_sizeof, METH_NOARGS, sizeof_doc},
    {"append",          (PyCFunction)listappend,  METH_O, append_doc},
    {"insert",          (PyCFunction)listinsert,  METH_VARARGS, insert_doc},
    {"extend",          (PyCFunction)listextend,  METH_O, extend_doc},
    {"pop",             (PyCFunction)listpop,     METH_VARARGS, pop_doc},
    {"remove",          (PyCFunction)listremove,  METH_O, remove_doc},
    {"index",           (PyCFunction)listindex,   METH_VARARGS, index_doc},
    {"count",           (PyCFunction)listcount,   METH_O, count_doc},
    {"reverse",         (PyCFunction)listreverse, METH_NOARGS, reverse_doc},
    {"sort",            (PyCFunction)listsort,    METH_VARARGS | METH_KEYWORDS, sort_doc},
    {NULL,              NULL}           /* sentinel */
};

In the file that defines the list, the C functions corresponding to Python’s built-in list methods are defined. Below is a quick look at the implementation logic of these functions.

  • Initialization

Objects/listobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PyList_New(Py_ssize_t size)
{
    ...
    // 如果缓冲池非空, 从缓冲池取
    if (numfree) {
        numfree--;
        op = free_list[numfree];
        _Py_NewReference((PyObject *)op);
        ...
    } else {
        // 否则, 申请新的内存空间
        op = PyObject_GC_New(PyListObject, &PyList_Type);
        ...
    }
    if (size <= 0)
        op->ob_item = NULL;
    else {
        ...
        // 初始化
        memset(op->ob_item, 0, nbytes);
    }
    ...
}
  • append

Call flow: listappend -> app1 -> after list_resize +1, PyList_SET_ITEM

include/listobject.h

1
#define PyList_SET_ITEM(op, i, v) (((PyListObject *)(op))->ob_item[i] = (v))

Objects/listobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
    PyObject **items;
    size_t new_allocated;
    Py_ssize_t allocated = self->allocated;

    // 如果内存大小够用,并且新内存大小超过之前的一半,则需要分配新内存
    if (allocated >= newsize && newsize >= (allocated >> 1)) {
        assert(self->ob_item != NULL || newsize == 0);
        Py_SIZE(self) = newsize;
        return 0;
    }
    //否则分配新的内存空间
    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
    ...
}

list_resize() requests a bit more space than needed in order to avoid frequent memory operations; the growth trend is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, … In fact, memory is allocated by calling C’s realloc.

  • insert

Call flow: listinsert -> ins1 -> list_resize +1, then shift the elements and insert the new data.

Objects/listobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    ...
    if (list_resize(self, n+1) == -1)
        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;
}

The other methods are not listed one by one here.

4. How Dictionaries Are Implemented in Python

A dictionary in Python is implemented with a hash table. A hash table is an array whose index is obtained by applying a hash function to the key; collisions are resolved with the open addressing method.

  • Definition

Include/dictobject.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct {
    Py_ssize_t me_hash; //用于缓存 me_key 的哈希值,避免每次查询都需要计算 hash
    PyObject *me_key;
    PyObject *me_value;
} PyDictEntry;

typedef struct _dictobject PyDictObject;
struct _dictobject {
    PyObject_HEAD
    Py_ssize_t ma_fill; //表示所有激活元素(active entry)和虚拟元素(dummy entry)的计数。
    Py_ssize_t ma_used; //所有激活元素的计数
    Py_ssize_t ma_mask; //哈希表的位掩码,这个表中包含 ma_mask + 1 个哈希槽(slot)    PyDictEntry *ma_table; //PyDictEntry 结构体的数组, PyDictEntry 包含 key 对象、value 对象,以及 key 的哈希;
    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);//用于查找 key 的函数指针
    PyDictEntry ma_smalltable[PyDict_MINSIZE]; //最小有 8 个槽的哈希表
};

The above is the state transition diagram for a dictionary key. If a key is deleted, the entry is set to dummy; it is not really removed. An entry in the dummy state becomes active again after a new element is inserted.

  • Basic functions

Objects/dictobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static PyMethodDef mapp_methods[] = {
    ...
     sizeof__doc__},
    {"has_key",         (PyCFunction)dict_has_key,      METH_O,
     has_key__doc__},
    {"get",         (PyCFunction)dict_get,          METH_VARARGS,
     get__doc__},
    {"setdefault",  (PyCFunction)dict_setdefault,   METH_VARARGS,
     setdefault_doc__},
    {"pop",         (PyCFunction)dict_pop,          METH_VARARGS,
     pop__doc__},
    {"popitem",         (PyCFunction)dict_popitem,      METH_NOARGS,
     popitem__doc__},
    {"keys",            (PyCFunction)dict_keys,         METH_NOARGS,
    keys__doc__},
    {"items",           (PyCFunction)dict_items,        METH_NOARGS,
     items__doc__},
    {"values",          (PyCFunction)dict_values,       METH_NOARGS,
     values__doc__},
    {"viewkeys",        (PyCFunction)dictkeys_new,      METH_NOARGS,
     viewkeys__doc__},
    {"viewitems",       (PyCFunction)dictitems_new,     METH_NOARGS,
     viewitems__doc__},
    {"viewvalues",      (PyCFunction)dictvalues_new,    METH_NOARGS,
     viewvalues__doc__},
    {"update",          (PyCFunction)dict_update,       METH_VARARGS | METH_KEYWORDS,
     update__doc__},
    {"fromkeys",        (PyCFunction)dict_fromkeys,     METH_VARARGS | METH_CLASS,
     fromkeys__doc__},
    {"clear",           (PyCFunction)dict_clear,        METH_NOARGS,
     clear__doc__},
    {"copy",            (PyCFunction)dict_copy,         METH_NOARGS,
     copy__doc__},
    {"iterkeys",        (PyCFunction)dict_iterkeys,     METH_NOARGS,
     iterkeys__doc__},
    {"itervalues",      (PyCFunction)dict_itervalues,   METH_NOARGS,
     itervalues__doc__},
    {"iteritems",       (PyCFunction)dict_iteritems,    METH_NOARGS,
     iteritems__doc__},
    {NULL,              NULL}   /* sentinel */
};
  • Initialization

Objects/dictobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
PyObject *
PyDict_New(void)
{
    register PyDictObject *mp;
    // 初始化dummy
    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);
        }
        ...
    } else {
        // 否则,申请新的内存对象
        mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
        if (mp == NULL)
            return NULL;
        ...
    }
    // 设置搜索方法
    mp->ma_lookup = lookdict_string;
    ...
    return (PyObject *)mp;
}

A note here: the dictionary object’s buffer pool free_list is an array of length 80.

  • get

Call flow: dict_get -> lookdict_string -> lookdict, returning the value found.

Objects/dictobject.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static PyObject *
dict_get(register PyDictObject *mp, PyObject *args)
{
    ...
    ep = (mp->ma_lookup)(mp, key, hash);
    ...
    val = ep->me_value;
    ...
    return val;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static PyDictEntry *
lookdict(PyDictObject *mp, PyObject *key, register long hash)
{
    ...
    register PyDictEntry *freeslot;
    register size_t mask = (size_t)mp->ma_mask; // 掩码等于数组长度 - 1
    ...
    // 计算所在 entry 位置.
    i = (size_t)hash & mask;
    ep = &ep0[i];
    // 如果找到,则返回 entry
    if (ep->me_key == NULL || ep->me_key == key)
        return ep;

    // 如果是 dummy,赋值给 freeslot,freeslot 用来指向探测序列中第一个处于dummy 态的 entry。如果搜索失败,则返回 freeslot,其指向的 enery ->me_value 为  NULL
    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;
    }

    // 冲突时,二次探测
    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) {
            ...
        }
        else if (ep->me_key == dummy && freeslot == NULL)
            freeslot = ep;
    }
    ...
}

5. References


微信公众号
WRITTEN BY
微信公众号