一. 映射类型与可散列
dict
类型不但使用广泛,也是 Python 语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影。因为字典至关重要,Python 对它的实现做了高度优化,而 散列表则是字典类型性能出众的根本原因 ,并且集合 set
的实现其实也依赖于散列表。
collections.abc
模块中有 Mapping
和 MutableMapping
这两个抽象基类,它们的作用是为 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__
方法。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
原子不可变数据类型:str
、bytes
和 数值类型,都是可散列类型,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}
示例 4 用 setdefault
方法处理找不到的键
当字典 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)
如果单词不存在,把单词和一个空列表放进映射,然后返回这个空列表。整个过程只需要一次查询。