《流畅的Python》Data Structures--第3章 dict 和 set

dict and set

字典数据活跃在所有的python程序背后,即使你的源码里并没有直接使用它。

和dict有关的内置函数在模块builtins的__dict__内。

>>> __builtins__
<module 'builtins' (built-in)>
>>> __builtins__.__dict__

 

dict之所以在python中起到至关重要的作用,是因为Hash table。

本章内容:

  • 常见方法
  • 如何处理找不到的key
  • dict变种
  • set, frozenset
  • Hash table 工作原理
  • hash table的潜在影响。

 

Generic Mapping Types 泛映射类型

在python中只有一种标准映射类型: dict。下面说的是泛映射类型。

 

collections.abc模块提供了Mapping及其子类MutableMapping,用于formalize规范dict和相关类型的接口。

 

 

>>> from collections import *
>>> UserDict
<class 'collections.UserDict'>
>>> UserDict.__bases__
(<class 'collections.abc.MutableMapping'>,)
>>> UserDict.__class__
<class 'abc.ABCMeta'>

 

collections.UserDict的父类是MutableMapping。

 

标准库中所有的映射类型都是利用dict来实现。

 

key必须是hashable的数据类型。因为key必须是不可变的对象。

 

什么是Hashable? (点击链接,看文档说明)

  • 如果说一个对象是hashable,那么在这个对象的整个生命中,它的hash value是不可变的。__hash__()
  • 这个对象可以和其他对象进行比较。__eq__()

根据这个定义,str, bytes, 数值类都是hashable的。元祖的所有元素都是hashable的话,元祖也是hashable。

>>> a = {}
>>> hash(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> a = []
>>> hash(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> a = (1, 2, (1, 2))
>>> hash(a)
-1429464707349485113
>>> a = "hello"
>>> hash(a)
1326837820661389949

 

使用hash()可知一个对象是否是hashable。返回一个hash value,如果不可hashable,报错。

⚠️其实就是说这个对象能否作为一个字典的key 使用,或一个set的内部项使用。

 

什么是mapping(文档)

mapping术语指的就是一种支持任意键查找并实现了 Mapping 或 MutableMapping 抽象基类 中所规定方法的容器对象。dict, OrderedDict等。

 

stackoverflow上的回答:什么是一个映射对象

a mapping object (an object that supports PyMapping_Keys() and PyObject_GetItem())

第一个函数,来自映射协议。第二个来自对象协议。

 

dict Comprehensions 

list有推导,dict也有推导,一种更简化的代码写法。

 

后面的章节:(略过,未学习)

  • 常用方法和setdefault
  • 变种:OrderedDict, ChainMap, Counter
  • 子类UserDict: 更适合自定义映射类。

 

 

3.8集合 Set Theory

set涉及set和frozenset。

Python历史中,比较新的概念,使用频率也比较低。

一个set的本质就是许多唯一对象的集合collection。它的基本用途就是去除重复。

>>> l = ['spam', 'spam', 'eggs']
>>> set(l)
{'eggs', 'spam'}
>>> list(set(l))
['eggs', 'spam']

⚠️set中的元素必须是hashable, 即体现出唯一性。

👆例子,set可以转化为list。使用tuple(l)也可以转化为tuple。

 

set类可以使用基础的 infix operators(中缀运算符),包括+, - ,& ,|

>>> s = {1}
>>> a = {2}
>>> s & a
set()
>>> s | a
{1, 2}

 

 

 

set Literals 

class set()

创建一个不带参数的set,需要使用构建器: set(参数1)。

⚠️使用set()必然调用set函数, 速度比直接使用{}慢,可以通过dis.dis()反编译字节码看一下过程。

 

Set Comprehensions 

又见推导式:

>>> from unicodedata import name
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), " ")}
{'=', '¤', '¢', '±', '®', '¬', '+', '§', '©', '°', '#', '¥', '×', '>', '£', '÷', '$', '%', 'µ', '', '<'}

#chr(i) ,  i是整数,返回对应的Unicode码的字符串格式。

 

 


 

 

Difference between abstract class and interface in Python

Python没有正式的接口协议,不像java有明显的抽象和接口。如果想要实现一个接口,其实就是一个抽象类。

Java使用接口是因为它没有多重继承。而Python可以有多重继承。

 

 

我的理解:

  • 抽象类,就是一些类共有的特征。
  • 接口,接口中的方法,被不相关的类使用,仅仅是因为这些类都要用到这个接口中的方法。

 

  • 抽象类,提供方法给它的子类。
  • 接口,提供方法给第三方使用者。

 

  • 抽象类可以既有抽象方法也可以有concrete method有型的方法。
  • 接口,只有一堆抽象方法。

 


 

 

set类中的add方法,来自MutableSet吗?

set类的父类就是object。除了自身方法,就是object的方法。

MutableSet继承自Set,而Set继承Collection, Collection多重继承Size, Container, Iterable。

所以两者没不相关的。 

 

看源码:https://github.com/python/cpython/blob/master/Objects/setobject.c

static PyMethodDef set_methods[] = {
    {"add",             (PyCFunction)set_add,           METH_O,
     add_doc},
    {"clear",           (PyCFunction)set_clear,         METH_NOARGS,
     clear_doc},
    {"__contains__",(PyCFunction)set_direct_contains,           METH_O | METH_COEXIST,
     contains_doc},
    {"copy",            (PyCFunction)set_copy,          METH_NOARGS,
     copy_doc},
    {"discard",         (PyCFunction)set_discard,       METH_O,
     discard_doc},

#后面的代码略
set_pop(PySetObject *so, PyObject *Py_UNUSED(ignored))
{
    /* Make sure the search finger is in bounds */
    setentry *entry = so->table + (so->finger & so->mask);
    setentry *limit = so->table + so->mask;
    PyObject *key;

    if (so->used == 0) {
        PyErr_SetString(PyExc_KeyError, "pop from an empty set");
        return NULL;
    }
    while (entry->key == NULL || entry->key==dummy) {
        entry++;
        if (entry > limit)
            entry = so->table;
    }
    key = entry->key;
    entry->key = dummy;
    entry->hash = -1;
    so->used--;
    so->finger = entry - so->table + 1;   /* next place to start */
    return key;
}

 

上面的是set实例的pop方法的源码,返回key。可以在交互看到:

>>> a = {1, 2}
>>> a.pop()
1

 


 

 

dict, set的速度优势

除了Cpython做的优化,这里主要讲述hash table。要理解它们的优势和缺点,也要讲述它们背后的hash tables。

  • efficient
  • 为什么是无序的unordered
  • 为何不是所有对象都能做key
  • Why does the order of the dict keys or set elements depend on insertion order, and may change during the lifetime of the structure?
  • ⚠️,为何不要在迭代时,进行add操作?

 

A Performance Experiment(一个实验,表明用dict,set查找的速度比list快多了)

 

Hash Tables in Dictionaries

概述一下如何用hashtable来实现一个dict。

  1. hash table是一个sparse array(有很多空白元素cell的数组)。这种cell也叫做buckets桶/表元。
  2. 一个dict的hash table,每个key/value对儿都占据一个bucket。每个bucket由对key的引用和对value的引用2部分组成。
  3. 所有的bucket的占用空间大小都一样。因此可以通过偏移量来查找buckets。⚠️不理解
  4. 一个hash table,有三分之一的cell/buckets是空的。如果到达阀值,hashtable会被复制到更大的空间。
  5. 如果要把一个对象放入hash table,首先计算它的key的hash值,使用内建函数hash()得到hash值。

 

散列值和相等性质

如果2个对象是相等的,那么散列值也是相等的。但是整数1和浮点1.0比较 1 == 1.0为真,但它们内部的结构是不一样的。所以如果它们在一个散列表内,多作为key,就会产生散列冲突。但如果散列表足够大,就能避免。

⚠️其实还是不清楚。

 

Hash Tables algorithm

  1. 为了获得my_dict[search_key]的值,Python 首先调用hash(search_key)得到hash value。
  2. 用hash value的最低几位的数字当作偏移量,在散列表中找bucket。⚠️偏移量???
    • 找到了bucket,但是,是空的,raise KeyError
    • 非空,则检查search_key==found_key 是否为真,如果相等则返回foud_value。
    • 如果不匹配,这是a hash collision。

遇到散列冲突:

原因是,寻找bucket的方法的原因。如果冲动,就重新来,从hash value内再取一段数字。

⚠️,具体偏移量和散列值的关系,本节未讲清楚。

我的理解,往hash table内新插入一个对象,如果这个对象的key的hash value的部分bit数字和bucket的内存位置的部分数字匹配来,那么就把这个对象的引用存入这个bucket中。当查找这个对象,也是如此匹配。

 

dict的实现及其导致的后果

1. keys必须是可散列的。

2 dict在内存上开销巨大。 因为使用了散列表,而散列表必须是稀疏的。

3.key的查询速度很快。典型的空间换时间。

4.key的顺序取决于插入时的顺序。

5.⚠️往字典里添加新的键可能会改变已有key的顺序。原因就是,可能会产生扩容操作,那么会重新把字典的已有元素添加到新的表里,这个过程有可能会发生散列冲突,导致新散列表中键的次序变化。所以在迭代时不要修改dict.

 

所有dict的特点也适用于set


 

 

总结

Dictionaries are a keystone of Python。

除了基本dict类,标准库提供了特殊的map类,便于扩展,和特殊用处。

 

update()是一个强大的新增和修改的方法。

 

⚠️,本章没有过于全部学习。学习了了6成的内容。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2019-12-08 20:46  Mr-chen  阅读(267)  评论(0编辑  收藏  举报