第3章 字典和集合

82 阅读4分钟

字典和集合

散列

  • 如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__()方法。另外可散列对象还要有__qe__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
  • 只有当一个元组所包含的所有元素都是可散列类型的情况下,它才是可散列的。
  • 散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组),所以字典、集合在空间上的效率低下。因此如果需要存放数量巨大的记录,那么放在自由元组或是具名元组构成的列表中会是比较好的选择;最好不要根据 JSON 的风格用由字典组成的列表来存放这些记录。一是避免了散列表的空间损耗,二是没有必要把每个字段的名字都存一遍。

字典推导

>>> DIAL_CODES = [ 
... (86, 'China'),
... (91, 'India'),
... (1, 'United States'),
... (62, 'Indonesia'),
... (55, 'Brazil'),
... (92, 'Pakistan'),
... (880, 'Bangladesh'),
... (234, 'Nigeria'),
... (7, 'Russia'),
... (81, 'Japan'),
... ]
>>> country_code = {country: code for code, country in DIAL_CODES} 
>>> country_code
{'China': 86, 'India': 91, 'Bangladesh': 880, 'United States': 1,
'Pakistan': 92, 'Japan': 81, 'Russia': 7, 'Brazil': 55, 'Nigeria':
234, 'Indonesia': 62}
>>> {code: country.upper() for country, code in country_code.items()
... if code < 66}
{1: 'UNITED STATES', 55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA'}

常见映射方法

dictdefaultdictOrderedDict
d.clear()移除所有元素
d.__contains__(k)检查k是否在d中
d.copy()浅复制
d.__copy__()用于支持copy.copy
d.default_factory__missing__函数中被调用的函数,用以给未找到的元素设置值
d.__delitem__(k)del d[k], 移除键为k的元素
d.fromkeys(it, [initial])将迭代器it里的元素设置为映射里的键,如果有initial参数,就把它作为这些键对应的值(默认值是None
d.get(k, [default])没有键 k, 则返回None或者default
d.__getitem__(k)让字典 d 能用d[k]的形式返回键 k 对应的值
d.items()返回 d 里所有的键值对
d.__iter__()获取键的迭代器
d.keys()获取所有的键
d.__len__()可以用len(d)的形式得到字典里键值对的数量
d.__missing__(k)__getitem__找不到对应键的时候,这个方法会被调用
d.move_to_end(k, [last])把键为 k 的元素移动到最靠前或者最靠后的位置(last 的默认值是 True)
d.pop(k, [default])返回键k所对应的值,然后移除这个键值对,如果没有这个键,返回None或者default
d.popitem()随机返回一个键值对并从字典里移除它
d.__reversed__()返回倒序的键的迭代器
d.setdefault(k, [default])若字典里有键 k则返回对应的值;若无则d[k] = default,然后返回default
d.__setitem__(k, v)实现d[k] = v操作,把对应的值设为 v
d.update(m, [**kargs])m 可以是映射或者键值对迭代器,用来更新 d 里对应的条目
d.values()返回字典里所有的值
  • d.update(m, [**kargs])

    该方法处理参数 m 的方式是典型的“鸭子类型”。函数首先检查 m 是否有 keys 方法,如果有,那么 update 函数就把它当作映射对象来处理。否则,函数会退一步,转而把 m 当作包含了键值对 (key, value) 元素的迭代器。

    In [24]: _dict = dict.fromkeys(range(10), "value")
    
    In [25]: _dict
    Out[25]: 
    {0: 'value',
     1: 'value',
     2: 'value',
     3: 'value',
     4: 'value',
     5: 'value',
     6: 'value',
     7: 'value',
     8: 'value',
     9: 'value'}
    
    In [26]: _dict.update(zip(range(100, 110), range(200, 210)))
    
    In [27]: _dict
    Out[27]: 
    {0: 'value',
     1: 'value',
     2: 'value',
     3: 'value',
     4: 'value',
     5: 'value',
     6: 'value',
     7: 'value',
     8: 'value',
     9: 'value',
     100: 200,
     101: 201,
     102: 202,
     103: 203,
     104: 204,
     105: 205,
     106: 206,
     107: 207,
     108: 208,
     109: 209}
    
  • setdefault(k, [default])

    为字典更新键值对时,使用setdefault是比较好的办法。

    my_dict.setdefault(key, []).append(new_value)  #方法一
    
    if key not in my_dict:	#方法二
        my_dict[key] = []
    my_dict[key].append(new_value)
    

    方法一、二的效果是一样的,不过后者至少要进行两次键查询——如果键不存在的话就是三次,而使用setdefault只需要一次就可以完成整个操作。

  • d.default_factory

    在创建一个defaultdict时接受一个方法作为default_factorydefault_factory只会在__getitem__里被调用,在其他的方法里完全不会发挥作用。例如:k 是个找不到的键,d[k]这个表达式会调用default_factory创造某个默认值,而d.get(k)则会返回None。背后的原理为,在找不到键时__getitem__会调用__missing__,而__missing__则会调用default_factory方法。

  • __missing__

    所有的映射类型在处理找不到的键的时候,都会牵扯到 __missing__方法。这也是这个方法称作“missing”的原因。虽然基类 dict 并没有定义这个方法,但是 dict 是知道有这么个东西存在的。也就是说,如果有一个类继承了 dict,然后这个继承类提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。

字典的变种

  • collections.Counter

    >>> ct = collections.Counter('abracadabra')
    >>> ct
    Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
    >>> ct.update('aaaaazzz')
    >>> ct
    Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
    >>> ct.most_common(2)
    [('a', 10), ('z', 3)]
    
  • collections.UserDict

    这个类其实就是把标准 dict 用纯 Python 又实现了一遍。跟 OrderedDictChainMapCounter 这些开箱即用的类型不同,UserDict 是让用户继承写子类的。

子类化UserDict

​ 更倾向于从 UserDict 而不是从 dict 继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict 就不会带来这些问题。

​ 另一个值得注意的地方是,UserDict并不是dict的子类,但是UserDict有个一叫作data的属性(只有在实例化一个对象后,对象才会有data属性),即如下所示:该属性是dict的实例,这个属性实际上是UserDict最终存储数据的地方。

In [33]: set(dir(collections.UserDict())) - set(dir(collections.UserDict))
Out[33]: {'data'}

不可变映射类型

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

>>> from types import MappingProxyType
>>> d = {1:'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1] ➊
'A'
>>> d_proxy[2] = 'x' ➋
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'})
>>> d_proxy[2]
'B'

集合论

​ 集合中的元素是可散列的,set类型本身是不可散列的,但是frozenset可以,因此可以创建一个包含不同frozensetset

  • 句法陷阱

    ​ 如果要创建一个空集,必须用不带任何参数的构造方法set()。如果只是写成{}的形式,创建的实际上是一个空字典。

  • 构建速度

    • {} 快于set([])
    • {1, 2, 3} 这种字面量句法相比于构造方法(set([1, 2, 3]))要更快且更易读。后者的速度要慢一些,因为 Python 必须先从 set 这个 名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。但是如果是像 {1, 2, 3} 这样的字面量,Python 会利用一个专门的叫作 BUILD_SET 的字节码来创建集合。
  • 集合推导

    略:懂得都懂

  • 集合的特点

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