字典这个数据结构活跃在所有 Python 程序的背后,即便你的源码里并没有直接用到它。

--- A.M.Kuchling

什么是可散列的数据类型

  • 如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__() 方法。另外可散列的对象还要有__qe__() 方法,这样才能跟其他键做比较。如果两个可散列的对象是相等的,那么它们的散列值一定是一样的。
  • 原子不可变数据类型(str, bytes 和数值类型) 都是可散列类型,frozenset 也是可散列的。元组只有当它包含的所有元素都是可散列类型的情况下,它才是可散列的。

字典推导

一个例子

>>> A = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> a_dict = {number: val for number, val in A}
>>> a_dict
{1: 'a', 2: 'b', 3: 'c'}

常见的映射方法

setdefault 处理找不到的键

一个例子

>>> index = {}
>>> occurrences = index.get('hello', []) #第一次查询
>>> occurrences
[]
>>> occurrences.append(1)
>>> index['hello'] = occurrences # 第二次查询

而当使用 setdefault 时仅需查询一次。

>>> index = {}
>>> index.setdefault('hello', []).append(1) # 只调用一次
>>> index['hello']
[1]

映射的弹性键查询

  • 为了方便起见,就算某个键在映射里不存在时,我们也希望在通过这个键读取值的时候能得到一个默认值。

defaultdict 处理找不到的键的一个选择

  • 在实例化一个 defaultdict 的时候,需要给构造方法提供一个可调用的对象,这个可调用对象会在 __getitem__ 碰到找不到的键的时候被调用,让 __getitem__ 返回某种默认值。
  • 这个用来生成默认值的可调用对象存放在名为 default_factory 的实例属性里。
  • 所有这一切背后的功臣其实是特殊方法 __missing__。它会在 defaultdict 遇到找不到的键的时候调用 default_factory, 而实际上这个特性是所有映射类型都可以选择去支持的。

一个例子

>>> import collections
>>> index = collections.defaultdict(list)
>>> index['hello'].append(1) #只调用一次
>>> index['hello']
[1]

注意

  • defaultdict 里的 default_factory 只会在 __getitem__ 里被调用,在其他方法里面完全不会发挥作用。比如 dd 是个 defaultdict, k 是个找不到的键, dd[k] 这个表达式会调用 default_factory 创造某个默认值,而 dd.get(k) 则会返回 None

特殊方法 __missing__

  • 当有一个类继承了 dict, 然后这个继承类提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候, Python 就会自动调用它, 而不是抛出一个 KeyError 异常。

注意

__missing__ 方法只会被 __getitem__ 调用 (比如在表达式d[k]中)。 提供 __missing__ 方法对 get 或者 __contains__ (in 运算符会用到这个方法) 这些方法的使用没有影响。

一个例子

class MyDict(dict):

    def __missing__(self, key):
        return "missing..."

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default


a = MyDict()

print(a['apple'])
print(a.get('apple'))
print('apple' in a)

"""
output:
missing...
missing...
False
"""

字典的变种

collectios.OrderedDict

  • 添加键的时候会保持顺序,因此键的迭代次序总是一致的。

collectios.ChainMap

  • 该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到位置。

一个例子

Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import ChainMap
>>> a = {"x":1, "z":3}
>>> b = {"y":2, "z":4}
>>> c = ChainMap(a, b)
>>> c
ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
>>> c['x']
1
>>> c['z']
3
>>> c['z'] = 8
>>> c
ChainMap({'x': 1, 'z': 8}, {'y': 2, 'z': 4})

collections.Counter

  • 这个映射类型会给键准备一个整数计数器,每更新一个键的时候都会增加这个计数器。

一个例子

>>> import collections
>>> ct = collections.Counter('absfbababsdgva')
>>> ct
Counter({'a': 4, 'b': 4, 's': 2, 'd': 1, 'g': 1, 'f': 1, 'v': 1})
>>> ct.update('asbsababaera')
>>> ct
Counter({'a': 9, 'b': 7, 's': 4, 'e': 1, 'd': 1, 'g': 1, 'f': 1, 'r': 1, 'v': 1})

collections.UserDict

  • 这个类其实就是把标准 dict 用纯Python 又实现了一遍。
  • UserDict 是让用户继承写子类的。

不可变映射类型

  • 从 Python3.3 开始, types 模块中引入了一个封装类名叫 MappingProxyType。 如果给这个类一个映射, 它会返回一个只读的映射视图。虽然是个只读视图,但它是动态的。这意味着如果对原映射做出了改动,我们能够通过这个视图观察到,但无法通过这个视图对原映射做出修改。

一个例子

Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from types import MappingProxyType
>>> d = {1:'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[2] = 'B'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>>
>>> d[2]='B'
>>> d_proxy
mappingproxy({1: 'A', 2: 'B'})

集合

集合的初始化

>>> s = {1} # 更快
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s = set([1]) # 慢
>>> s
{1}

dis.dis (反汇编函数)来看看两个方法的字节码的不同

>>> from dis import dis
>>> dis('{1}')
  1           0 LOAD_CONST               0 (1)
              2 BUILD_SET                1
              4 RETURN_VALUE
>>> dis('set([1])')
  1           0 LOAD_NAME                0 (set)
              2 LOAD_CONST               0 (1)
              4 BUILD_LIST               1
              6 CALL_FUNCTION            1
              8 RETURN_VALUE
  • 前者更快更易读,Python 会利用一个专门的叫作 BUILD_SET 的字节码来创建集合。
  • 后者Python必须先从 set 这个名字查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。

集合推导

一个例子

>>> s = {i for i in range(3, 40)}
>>> s
{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}

dictset 的背后

散列表

  • 散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组),在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。在 dict 的散列表当中, 每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有标语那的大小一致,所以可通过偏移量来读取某个表元。
  • Python 会设法保证大概有三分之一的表元是空的,所以在要达到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。

散列值和相等性

  • 内置的 hash() 方法可以用于所有的内置类型对象。 如果是自定义对象调用 hash() 的话,实际上运行的是自定义的 __hash__如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。

散列表算法

Fluent Python 最新版_默认值

dict 的实现及其导致的结果

  1. 键必须是可散列的, 一个可散列的对象必须满足一下要求。
  1. 支持 hash() 函数,并且通过 __hash__() 方法所得到的散列值是不变的。
  2. 支持通过 __eq__() 方法来检测相等性。
  3. a == b 为真, 则 hash(a) == hash(b) 也为真。
  1. 字典在内存上的开销巨大。
  2. 键查询很快
  3. 键的次序取决于添加顺序
  4. 往字典里添加新键可能会改变已有键的顺序
  5. 不要对字典同时进行迭代和修改

set 的实现以及导致的结果

  1. 集合里的元素必须是可散列的。
  2. 集合很消耗内存。
  3. 可以很高效地判断元素是否存在于某个集合。
  4. 元素的次序取决于被添加到集合里的次序。
  5. 往集合里添加元素,可能会改变集合里的已有元素的次序。

终于写完了,下一章将会介绍文本和字节序列。