Python 字典和集合 - 认识字典

一. 映射类型与可散列

dict 类型不但使用广泛,也是 Python 语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影。因为字典至关重要,Python 对它的实现做了高度优化,而 散列表则是字典类型性能出众的根本原因 ,并且集合 set 的实现其实也依赖于散列表。

collections.abc 模块中有 MappingMutableMapping 这两个抽象基类,它们的作用是为 dict 和其他类似的类型定义形式接口。collections.abc 中的 MutableMapping 和它的超类的 UML 类图(箭头从子类指向超类)如下:

但是,我们并不会直接继承这些抽象基类,而是直接对 dict 或是 collections.User.Dict 进行扩展。这些抽象基类的主要作用是作为形式化的文档,定义了构建一个映射类型所需要的最基本的接口。此外,还可以跟
isinstance 一起被用来判定某个数据是不是广义上的映射类型:

>> from collections.abc import Mapping
>> d = {}
>> isinstance(d, Mapping)
True
>> L = []
>> isinstance(L, Mapping)
False
>> S = set()
>> isinstance(S, Mapping)
False

标准库里的所有映射类型都是利用 dict 来实现的,因此,它们有个共同的限制:只有可散列的数据类型才能用作这些映射里的键

可散列类型的定义:如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现 __hash__ 方法。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。

原子不可变数据类型:strbytes 和 数值类型,都是可散列类型,frozenset 也是可散列的,根据其定义:frozenset 里只能容纳可散列类型。对于元组,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的:

>> t1 = (1,2,(3,4))
>> hash(t1)
-2725224101759650258
>> t2 = (1,2,[3,4])
>> hash(t2)
TypeError: unhashable type: 'list'
>> t3 = (1,2,frozenset([3,4]))
>> hash(t3)
-4138728974339688815

一般来讲用户自定义的类型,其实例都是可散列的,散列值就是它们的 id 函数的返回值,因此所有这些对象在比较的时候都是不相等的。如果一个对象实现了 __eq__ 方法,并且在方法中用到了这个对象的内部状态的话,那么只有当所有这些内部状态都是不可变的情况下,这个对象才是可散列的。

二. 字典的使用

示例 1 字典的构造方法

>> d = dict(name='alex', age=18, score=99)
>> d
{'age': 18, 'name': 'alex', 'score': 99}
>> d1 = dict(zip(['name', 'age', 'score'], ['alex', 18, 99]))
>> d1
{'age': 18, 'name': 'alex', 'score': 99}
>> d2 = dict([('name', 'alex'), ('age', 18), ('score', 99)])
>> d2
{'age': 18, 'name': 'alex', 'score': 99}
>> d3 = {'age': 18, 'name': 'alex', 'score': 99}
>> d3
{'age': 18, 'name': 'alex', 'score': 99}
>> d == d1 == d2 == d3
True

示例 2 字典推导

>> L = [('name', 'alex'), ('age', 18), ('score', 99)]
>> {key.upper(): value for key, value in L}
{'AGE': 18, 'NAME': 'alex', 'SCORE': 99}

Python 中的字典也为我们提供了非常多使用的方法,其中常见方法总结如下:

常见方法 方法说明
d.clear() 移除所有元素
d.__contains__(k) 检查 k 是否在 d
d.copy() 浅复制
d.__delitem__(k) del d[k] 移除键为 k 的元素
d.fromkeys(it, [initial]) 将迭代器 it 里的元素设置为映射里的键,如果有 initial 参数,就把它作为这些键对应的值(默认是 None
d.get(k, [default]) 返回键 k 对应的值,如果字典里没有 k ,则返回 None 或者 default
d.__getitem__(k) 让字典 d 能用 d[k] 的形式返回键 k 对应的值
d.items() 返回 d 里所有的键值对
d.__iter__() 获取键的迭代器
d.keys() 获取所有的键
d.__len__() 可以用 len(d) 的形式得到字典里键值对的数量
d.pop(k, [default]) 返回键 k 所对应的值,然后移除这个键值对。若果没有这个键,返回 None 或者default
d.popitem() 随机返回一个键值对,并从字典里移除它
d.setdefault(k, [default]) 若字典里有键 k,则把它对应的值设为 default ,然后返回这个值;若无,则让 d[k] = default ,然后返回 default
d.__setitem__(k, v) 实现 d[k] = v 的操作,把 k 对应的值设为 v
d.update(m, [**kwargs]) m 可以是映射或者键值对迭代器,用来更新 d 里对应的条目
d.values() 返回字典里的所有值

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

Python 里大多数映射类型的构造方法都采用了类似的逻辑,因此你既可以用一个映射对象来新建一个映射对象,也可以用包含 (key, value) 元素的可迭代对象来初始化一个映射对象。

示例 3 update 方法的使用

>> L
[('name', 'alex'), ('age', 18), ('score', 99)]
>> d = dict(L)
>> d.update([('name', 'bob'), ('gender', 'male')])
>> d
{'age': 18, 'gender': 'male', 'name': 'bob', 'score': 99}
>> d.update({'score': 100, 'profession': 'programmer'})
>> d
{'age': 18,
 'gender': 'male',
 'name': 'bob',
 'profession': 'programmer',
 'score': 100}

示例 4setdefault 方法处理找不到的键
当字典 d[k] 不能找到正确的键的时候,Python 会抛出异常,这个行为符合 Python 所信奉的“快速失败”哲学。

也许每个 Python 程序员都知道可以用 d.get(k, default) 来代替 d[k] ,给找不到的键一个默认的返回值,这比处理 KeyError 要方便不少。但是要更新某个键对应的值的时候,不管使用 __getitem__ 还是 get 都会不自然,而且效率低。

首先,dict.get 并不是处理找不到的键的最好方法:

import sys
import re
 
WORD_RE = re.compile(r'\w+')
 
index = {}
with open('example.txt', encoding='utf-8') as f:
    for line_no, line in enumerate(f, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start()+1
            location = (line_no, column_no)
            occurrences = index.get(word, [])
            occurrences.append(location)
            index[word] = occurrences

for word in sorted(index, key=str.upper):
    print(word, index[word])

其中 example.txt 文件的内容如下:

运行结果:

Beautiful [(1, 1)]
better [(1, 14), (2, 13), (3, 11), (4, 12), (5, 9), (6, 11)]
complex [(3, 23)]
Complex [(4, 1)]
complicated [(4, 24)]
dense [(6, 23)]
Explicit [(2, 1)]
Flat [(5, 1)]
implicit [(2, 25)]
is [(1, 11), (2, 10), (3, 8), (4, 9), (5, 6), (6, 8)]
nested [(5, 21)]
Simple [(3, 1)]
Sparse [(6, 1)]
than [(1, 21), (2, 20), (3, 18), (4, 19), (5, 16), (6, 18)]
ugly [(1, 26)]

上述的代码,返回文件中,所有单词出现的位置。其中 index 字典的键为单词,值为一个2元元组构成的位置。在向字典中,添加元素时,我们使用了如下的代码:

occurrences = index.get(word, [])
occurrences.append(location)
index[word] = occurrences

如果 word 不存在时,返回一个空的列表,之后在列表中插入位置,最后将列表设置为字典中的元素。上述过程,我们一共对字典进行了 2 次查询。其实,通过 dict.setdefault 用一行即可解决获取和更新位置列表,而不进行第二次查找:

import sys
import re
 
WORD_RE = re.compile(r'\w+')
 
index = {}
with open('example.txt', encoding='utf-8') as f:
    for line_no, line in enumerate(f, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start()+1
            location = (line_no, column_no)
            index.setdefault(word, []).append(location)

for word in sorted(index, key=str.upper):
    print(word, index[word])

my_dict.setdefault(key, []).append(new_value) 如果单词不存在,把单词和一个空列表放进映射,然后返回这个空列表。整个过程只需要一次查询。

你可能感兴趣的:(Python 字典和集合 - 认识字典)