字典与集合高级用法

dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间实例的属性和函数的关键字参数中都可以看到字典的身影。

第三章 字典和集合

可散列的数据类型:

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象还需要实现__hash__()方法,另外可散列对象还需要有__eq__()方法,这样才能跟其他键做比较。

字典提供了很多种构造方法:

a = dict(one=1, two=2)
b = {'one':1, 'two':2}
c = dict(zip(['one', 'two'], [1, 2]))
d = dict([('one', 1), ('two', 2)])
e = dict({'one': 1, 'two': 2})

>>> a == b == c == d == e
True 

除了这些字面语法和灵活的构造元素以外,字典推导也可以用来建造新的 dict:

a = ['s', 'o', 's']
b = {k: v for k, v in enumerate(a)}

>>> print(b)
{0: 's', 1: 'o', 2: 's'}

使用 setdefault 来处理找不到的键

当字典 d[k] 不能找到正确的键时, Python 会抛出异常,这个行为符合 Python 所信奉的”快速失败“哲学,我们都知道可以用 d.get(k, default)来代替 d[k],给找不到的键一个默认的返回值。但是要更新某个键对应的值时,不管使用 __getitem__ 还是 get 都会不太自然,而且效率低。就像如下还没有经过优化的代码所显示的那样,dict.get 并不是处理找不到键的最好方式。

# -*- coding: utf-8 -*-
"""
@Desc    : 获取一段文本中的单词出现的频率以及出现的位置,如(1, 2), 横纵坐标都以 1 开始
"""

import sys
import re


# 创建正则 Pattern 对象,确保进行正则操作时不用重复创建 Pattern 对象
WORD_RE = re.compile(r'\w+')

index = {}

# sys.argv 是一个列表,sys.argv[0] 一般是"被调用的脚本名或全路径",sys.argv[1] 以及以后的都是传入的参数
# 如 python main.py ../../data/zen.txt
# sys.argv[0] 就是 main.py
# sys.argv[1] 就是 ../../data/zen.txt
with open(sys.argv[1], encoding='utf-8') as fp:
    # enumerate 有两个参数,第一个参数是一个 Iterable(可迭代对象),第二个参数 start 是开始位置,默认为 0
    for line_no, line in enumerate(fp, 1):
        # re.finditer(pattern, str) <=> pattern.finditer(str),返回一个iterator(迭代器)
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start() + 1
            location = (line_no, column_no)              
            # 提取 word 出现的情况,如果还没有该记录,返回[], 这不是一种好的写法
            occurrences = index.get(word, [])            # ①
            # 将新单词出现的位置添加到列表的后面
            occurrences.append(location)                 # ②
            # 将新列表放回到字典中,这里又牵扯到一次查询操作
            index[word] = occurrences                    # ③

# sorted 函数的 key=参数没有调用str.upper,而是把这个方法的引用传递给 sorted 函数
# 这样在排序的时候,就会按照str.upper处理后的方式进行排序。
for word in sorted(index, key=str.upper):
    print(word, len(index[word]))

上述处理单词的三行(① ② ③),通过 dict.setdefalut 可以只有一行就解决。

...
location = (line_no, column_no)
# 获取单词的出现情况列表,如果单词不存在,把单词和一个空列表放进映射,
# 然后返回这个空列表,这样就能在不进行第二次查找的情况下更新列表了
index.setdefault(word, []).append(location)
...

即:

my_dict.setdefault(key, []).append(new_value)

<=>

if key not in my_dict:
    my_dict[key] = []
my_dict[key].append(new_value)

只不过后者至少要进行两次键查询,如果键不存在的话,就是三次,用 setdefault 只需要一次就可以完成整个操作,以上是针对通过查找来插入新值时的最佳解决方式

字典的变种

  • collections.OrderedDict, 有序字典,在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 默认删除并返回的是字典里的最后一个元素,但是如果 popitem(last=False) 则删除并返回第一个被添加进的元素。
  • collections.ChainMap,该类型可以容纳数个不同的映射对象,在进行键查找的时候,这些对象会被当做一个整体被逐步查找,直到键被站到位置。
  • collections.Counter,该类型会给键准备一个整数计算器,每次更新一个键的时候都会增加这个计数器。所以这个类型还可以用来给可散列对象计数。Counter 实现了 + 和 - 运算符用来合并记录,还有 most_common([n]),它会按照次序返回映射里最常见的 n 个键和他们的计数。
  • collections.UserDict,该类就是把标准的 dict 用纯 Python 又实现了一遍,跟上述这些开箱即用的类型不同, UserDict 是让用户继承写子类(自定义映射类型)的。

创建自定义映射类型

就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。dict 会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法。

UserDict 并不是 dict 的子类,但是 UserDict 有一个叫作 data 的属性,是 dict 的实例,这个属性实际上是 UserDict 最终存储数据的地方。

'''
自定义字符串键字典
无论是添加,更新还是查询操作,StrKey都会把非字符串的键转换为字符串
'''
import collections


class StrKeyDict(collections.UserDict):
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]
    
    def __contains__(self, key):
        # 可以放心假设所有已经存储的键都是字符串,所以只要在self.data中查询就好了
        return str(key) in self.data
    
    def __setitem__(self, key, item):
        # self.data 为 dict 的实例
        self.data[str(key)] = item

字典中的散列表

散列表:其实是一个稀疏数组(总有空白元素的数组称为稀疏数组)。一般将散列表里的单元称为表元,在 dict 的散列表中,每个键值对都占用一个表元,每个表元有两个部分,分别是是对键、对值的引用,因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。

因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阀值的时候,原有的散列表会复制到一个更大的空间里。

字典的限制

  1. 键必须是可散列的
  2. 字典在内存上的开销巨大,由于散列表必须是稀疏的,这导致字典在空间上的效率低下,所以,如果你要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是比较好的选择。
  3. 往字典里添加新键可能会改变已有键的顺序。无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把已有的元素添加到新表里,这个过程可能会发生新的散列冲突,导致新建列表中键的次序变化。如果你在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环很有可能会跳过一些键——甚至是跳过那些字典中已经有的键。

集合

集合字面量,{1}, {1, 2},但是如果是空集,必须写成set()的形式。

集合推导,{ i for i in range(10)}

集合可用于去重。

集合的特点:

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

推荐阅读更多精彩内容