本章内容的大纲如下:
常见的字典方法
如何处理查找不到的键
标准库中 dict 类型的变种set 和 frozenset 类型
散列表的工作原理
散列表带来的潜在影响(什么样的数据类型可作为键、不可预知的
顺序,等等)
集合论
“集”这个概念在 Python 中算是比较年轻的,同时它的使用率也比较
低。set 和它的不可变的姊妹类型 frozenset 直到 Python 2.3 才首次以
模块的形式出现,然后在 Python 2.6 中它们升级成为内置类型。
本书中“集”或者“集合”既指 set,也指 frozenset。
当“集”仅指代 set 类时,我会用等宽字体表示
集合的本质是许多唯一对象的聚集。因此,集合可以用于去重:
>>> l = ['spam', 'spam', 'eggs', 'spam']
>>> set(l)
{'eggs', 'spam'}
>>> list(set(l))
['eggs', 'spam']
集合中的元素必须是可散列的,set 类型本身是不可散列的,但是
frozenset 可以。因此可以创建一个包含不同 frozenset 的 set。
除了保证唯一性,集合还实现了很多基础的中缀运算符。给定两个集合
a 和 b,a | b 返回的是它们的合集,a & b 得到的是交集,而 a - b
得到的是差集。合理地利用这些操作,不仅能够让代码的行数变少,还
能减少 Python 程序的运行时间。这样做同时也是为了让代码更易读,从
而更容易判断程序的正确性,因为利用这些运算符可以省去不必要的循
环和逻辑操作。
例如,我们有一个电子邮件地址的集合(haystack),还要维护一个
较小的电子邮件地址集合(needles),然后求出 needles 中有多少地
址同时也出现在了 heystack 里。借助集合操作,我们只需要一行代码
就可以了(见示例 3-10)。
示例 3-10 needles 的元素在 haystack 里出现的次数,两个变量都是 set 类型
found = len(needles & haystack)
如果不使用交集操作的话,代码可能就变成了示例 3-11 里那样。
示例 3-11 needles 的元素在 haystack 里出现的次数(作用和示
例 3-10 中的相同)
found = 0
for n in needles:
if n in haystack:
found += 1
示例 3-10 比示例 3-11 的速度要快一些;另一方面,示例 3-11 可以用在
任何可迭代对象 needles 和 haystack 上,而示例 3-10 则要求两个对
象都是集合。话再说回来,就算手头没有集合,我们也可以随时建立集
合,如示例 3-12 所示。
示例 3-12 needles 的元素在 haystack 里出现的次数,这次的代
码可以用在任何可迭代对象上
found = len(set(needles) & set(haystack))
# 另一种写法:
found = len(set(needles).intersection(haystack))
示例 3-12 里的这种写法会牵扯到把对象转化为集合的成本,不过如果
needles 或者是 haystack 中任意一个对象已经是集合,那么示例 3-12
的方案可能就比示例 3-11 里的要更高效。
以上的所有例子的运行时间都能在 3 毫秒左右,在含有 10 000 000 个元
素的 haystack 里搜索 1000 个值,算下来大概是每个元素 3 微秒。
除了速度极快的查找功能(这也得归功于它背后的散列表),内置的
set 和 frozenset 提供了丰富的功能和操作,不但让创建集合的方式丰富多彩,而且对于 set 来讲,我们还可以对集合里已有的元素进行修
改。在讨论这些操作之前,先来看一下相关的句法。
集合字面量
除空集之外,集合的字面量——{1}、{1, 2},等等——看起来跟它的
数学形式一模一样。如果是空集,那么必须写成 set() 的形式。
句法的陷阱
不要忘了,如果要创建一个空集,你必须用不带任何参数的构造方
法 set()。如果只是写成 {} 的形式,跟以前一样,你创建的其实
是个空字典。
在 Python 3 里面,除了空集,集合的字符串表示形式总是以 {…} 的形
式出现。
>>> s = {1}
>>> type(s)
<class 'set'>
>>> s
{1}
>>> s.pop()
1 >>> s
set()
像 {1, 2, 3} 这种字面量句法相比于构造方法(set([1, 2, 3]))要
更快且更易读。后者的速度要慢一些,因为 Python 必须先从 set 这个
名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构
造方法里。但是如果是像 {1, 2, 3} 这样的字面量,Python 会利用一
个专门的叫作 BUILD_SET 的字节码来创建集合。
用 dis.dis(反汇编函数)来看看两个方法的字节码的不同:
>>> from dis import dis
>>> dis('{1}') ➊
1 0 LOAD_CONST 0 (1)
3 BUILD_SET 1 ➋
6 RETURN_VALUE
>>> dis('set([1])') ➌
1 0 LOAD_NAME 0 (set) ➍
3 LOAD_CONST 0 (1)
6 BUILD_LIST 1
9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
12 RETURN_VALUE
➊ 检查 {1} 字面量背后的字节码。
➋ 特殊的字节码 BUILD_SET 几乎完成了所有的工作。
➌ set([1]) 的字节码。
➍ 3 种不同的操作代替了上面的
BUILD_SET:LOAD_NAME、BUILD_LIST 和 CALL_FUNCTION。
由于 Python 里没有针对 frozenset 的特殊字面量句法,我们只能采用
构造方法。Python 3 里 frozenset 的标准字符串表示形式看起来就像构
造方法调用一样。来看这段控制台对话:
>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
既然提到了句法,就不得不提一下我们已经熟悉的列表推导,因为也有
类似的方式来新建集合。
集合推导
Python 2.7 带来了集合推导(setcomps)和之前在 3.2 节里讲到过的字典
推导。示例 3-13 是个简单的例子。
示例 3-13 新建一个 Latin-1 字符集合,该集合里的每个字符的
Unicode 名字里都有“SIGN”这个单词
>>> from unicodedata import name ➊
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} ➋
{'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©','°', '+', '÷', '±', '>', '¬', '®', '%'}
➊ 从 unicodedata 模块里导入 name 函数,用以获取字符的名字。
➋ 把编码在 32~255 之间的字符的名字里有“SIGN”单词的挑出来,放到
一个集合里。
跟句法相关的内容就讲到这里,下面看看用于集合类型的丰富操作。