字典
字典 (dictionary) 类似于列表,但更通用。在列表中,索引位置必须是整数;在字典中,索引(几乎)可以是任何类型。
你可以将字典看作是一组索引(称为键 (keys))和一组值之间的映射。每个键映射到一个值。键和值的关联称为键值对 (key-value pair),有时也称为项 (item)。
举个例子,我们将构建一个将英语单词映射到西班牙语单词的字典,所以键和值都是字符串。
函数 dict
创建一个没有项的新字典。因为 dict
是一个内置函数的名称,你应该避免将其用作变量名。
>>> eng2sp = dict()
>>> print(eng2sp)
{}
花括号 {}
代表一个空字典。要向字典中添加项,你可以使用方括号:
>>> eng2sp['one'] = 'uno'
这行代码创建了一个从键 'one'
映射到值 “uno” 的项。如果我们再次打印字典,我们会看到一个键值对,键和值之间有一个冒号:
>>> print(eng2sp)
{'one': 'uno'}
这种输出格式也是一种输入格式。例如,你可以创建一个包含三个项的新字典。
>>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
>>> print(eng2sp)
{'one': 'uno', 'two': 'dos', 'three': 'tres'}
自 Python 3.7x 起,键值对的顺序与其输入顺序相同,即字典现在是有序结构。
但这并不重要,因为字典的元素从不使用整数索引进行访问。相反,你使用键来查找对应的值:
>>> print(eng2sp['two'])
'dos'
键 'two'
总是映射到值 “dos”,所以项的顺序无关紧要。
如果键不在字典中,你会得到一个异常:
>>> print(eng2sp['four'])
KeyError: 'four'
len
函数适用于字典;它返回键值对的数量:
>>> len(eng2sp)
3
in
运算符适用于字典;它告诉你某个东西是否作为键出现在字典中(作为值出现是不够的)。
>>> 'one' in eng2sp
True
>>> 'uno' in eng2sp
False
要查看某个东西是否作为值出现在字典中,你可以使用 values
方法,它返回值的类型可以转换为列表,然后使用 in
运算符:
>>> vals = list(eng2sp.values())
>>> 'uno' in vals
True
in
运算符对列表和字典使用不同的算法。对于列表,它使用线性搜索算法。随着列表变长,搜索时间与列表长度成正比增加。对于字典,Python 使用一种称为哈希表 (hash table) 的算法,它具有一个显著的特性:无论字典中有多少项,in
运算符花费的时间大致相同。我不会解释为什么哈希函数如此神奇,但你可以在 wikipedia.org/wiki/Hash_table 阅读更多相关内容。 1
练习 1: 下载文件副本
编写一个程序,读取 words.txt 中的单词并将它们存储为字典的键。值是什么并不重要。然后你可以使用 in
运算符作为一种快速检查字符串是否在字典中的方法。
字典作为计数器集合
假设给定一个字符串,你想计算每个字母出现的次数。有几种方法可以做到:
- 你可以创建 26 个变量,每个字母对应一个。然后你可以遍历字符串,对于每个字符,增加相应的计数器,可能使用链式条件。
- 你可以创建一个包含 26 个元素的列表。然后你可以将每个字符转换为一个数字(使用内置函数
ord
),使用该数字作为列表的索引,并增加相应的计数器。 - 你可以创建一个字典,以字符为键,以计数器为相应的值。第一次遇到某个字符时,你将向字典中添加一个新项。之后,你将增加现有项的值。
这些选项中的每一个都执行相同的计算,但每一个都以不同的方式实现该计算。
实现 (implementation) 是执行计算的一种方式;有些实现比其他的更好。例如,字典实现的一个优点是我们不必事先知道字符串中会出现哪些字母,我们只需要为实际出现的字母腾出空间。
下面是代码可能的样子:
word = 'brontosaurus'
d = dict()
for c in word:
if c not in d:
d[c] = 1
else:
d[c] = d[c] + 1
print(d)
我们实际上是在计算一个直方图 (histogram),这是一个用于表示计数器(或频率)集合的统计术语。
for
循环遍历字符串。每次循环,如果字符 c
不在字典中,我们创建一个键为 c
且初始值为 1 的新项(因为我们已经看到这个字母一次)。如果 c
已经在字典中,我们增加 d[c]
的值。
程序的输出如下:
{'b': 1, 'r': 2, 'o': 2, 'n': 1, 't': 1, 's': 2, 'a': 1, 'u': 2}
直方图表明字母“a”和“b”出现一次;“o”出现两次,依此类推。
字典有一个名为 get
的方法,它接受一个键和一个默认值。如果键出现在字典中,get
返回相应的值;否则返回默认值。例如:
>>> counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
>>> print(counts.get('jan', 0))
100
>>> print(counts.get('tim', 0))
0
我们可以使用 get
来更简洁地编写我们的直方图循环。因为 get
方法自动处理键不在字典中的情况,我们可以将四行代码缩减为一行,并消除 if
语句。
word = 'brontosaurus'
d = dict()
for c in word:
d[c] = d.get(c,0) + 1
print(d)
使用 get
方法简化这个计数循环最终成为 Python 中一个非常常用的“惯用法”(idiom),我们将在本书的其余部分多次使用它。所以你应该花点时间比较一下使用 if
语句和 in
运算符的循环与使用 get
方法的循环。它们做的事情完全相同,但后者更简洁。
字典和文件
字典的一个常见用途是统计文件中某些书面文本中单词的出现次数。让我们从一个非常简单的单词文件开始,这些单词取自《罗密欧与朱丽叶》的文本。
对于第一组示例,我们将使用一个不含标点的文本的缩短和简化版本。稍后我们将处理包含标点的场景文本。
But soft what light through yonder window breaks
It is the east and Juliet is the sun
Arise fair sun and kill the envious moon
Who is already sick and pale with grief
我们将编写一个 Python 程序来通读文件的行,将每行分解成单词列表,然后遍历该行中的每个单词,并使用字典对每个单词进行计数。
你会看到我们有两个 for
循环。外层循环读取文件的行,内层循环遍历该特定行上的每个单词。这是称为嵌套循环 (nested loops) 的模式的一个例子,因为其中一个循环是外层循环,另一个循环是内层循环。
因为每次外层循环进行一次迭代时,内层循环都会执行其所有迭代,所以我们认为内层循环迭代得“更快”,而外层循环迭代得更慢。
两个嵌套循环的组合确保我们将计算输入文件中每一行的每一个单词。
fname = input('Enter the file name: ')
try:
fhand = open(fname)
except:
print('File cannot be opened:', fname)
exit()
counts = dict()
for line in fhand:
words = line.split()
for word in words:
if word not in counts:
counts[word] = 1
else:
counts[word] += 1
print(counts)
# 代码: https://www.py4e.com/code3/count1.py
在我们的 else
语句中,我们使用了更紧凑的变量递增替代方案。counts[word] += 1
等同于 counts[word] = counts[word] + 1
。任何一种方法都可以用来将变量的值改变任意所需的量。类似的替代方案也存在于 -=
、*=
和 /=
。
当我们运行程序时,我们看到所有计数的原始转储,以未排序的哈希顺序显示。(romeo.txt 文件可在 www.py4e.com/code3/romeo.txt 获取)
python count1.py
Enter the file name: romeo.txt
{'But': 1, 'soft': 1, 'what': 1, 'light': 1, 'through': 1, 'yonder': 1,
'window': 1, 'breaks': 1, 'It': 1, 'is': 3, 'the': 3, 'east': 1, 'and': 3,
'Juliet': 1, 'sun': 2, 'Arise': 1, 'fair': 1, 'kill': 1, 'envious': 1,
'moon': 1, 'Who': 1, 'already': 1, 'sick': 1, 'pale': 1, 'with': 1,
'grief': 1}
浏览字典以查找最常见的单词及其计数有点不方便,所以我们需要添加一些更多的 Python 代码来获得对我们更有帮助的输出。
循环和字典
如果你使用字典作为 for
语句中的序列,它会遍历字典的键。这个循环打印每个键和对应的值:
counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
for key in counts:
print(key, counts[key])
输出如下:
chuck 1
annie 42
jan 100
再次强调,键是有序的。
我们可以使用这种模式来实现我们之前描述的各种循环惯用法。例如,如果我们想在字典中找到所有值大于十的条目,我们可以编写以下代码:
counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
for key in counts:
if counts[key] > 10 :
print(key, counts[key])
for
循环遍历字典的键,所以我们必须使用索引运算符来检索每个键对应的值。输出如下:
annie 42
jan 100
我们只看到值大于 10 的条目。
如果你想按字母顺序打印键,你首先使用字典对象中可用的 keys
方法创建一个字典键的列表,然后对该列表进行排序并遍历排序后的列表,查找每个键并按排序顺序打印出键值对,如下所示:
counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
lst = list(counts.keys())
print(lst)
lst.sort()
print(lst)
for key in lst:
print(key, counts[key])
输出如下:
['chuck', 'annie', 'jan']
['annie', 'chuck', 'jan']
annie 42
chuck 1
jan 100
首先你看到我们从 keys
方法得到的非字母顺序的键列表。然后我们看到 for
循环产生的按字母顺序排列的键值对。
高级文本解析
在上面使用文件 romeo.txt 的例子中,我们通过手动删除所有标点符号使文件尽可能简单。实际文本包含大量标点符号,如下所示。
But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun.
Arise, fair sun, and kill the envious moon,
Who is already sick and pale with grief,
由于 Python 的 split
函数查找空格并将单词视为由空格分隔的标记,因此我们会将单词“soft!”和“soft”视为不同的单词,并为每个单词创建单独的字典条目。
此外,由于文件包含大写字母,我们会将“who”和“Who”视为具有不同计数的不同单词。
我们可以通过使用字符串方法 lower
、punctuation
和 translate
来解决这两个问题。translate
是这些方法中最微妙的一个。以下是 translate
的文档:
line.translate(str.maketrans(fromstr, tostr, deletestr))
将 fromstr
中的字符替换为 tostr
中相同位置的字符,并删除所有在 deletestr
中的字符。fromstr
和 tostr
可以是空字符串,deletestr
参数可以省略。
我们不会指定 tostr
,但我们将使用 deletestr
参数来删除所有标点符号。我们甚至让 Python 告诉我们它认为是“标点符号”的字符列表:
>>> import string
>>> string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
translate
使用的参数在 Python 2.0 中有所不同。
我们对程序进行以下修改:
import string
fname = input('Enter the file name: ')
try:
fhand = open(fname)
except:
print('File cannot be opened:', fname)
exit()
counts = dict()
for line in fhand:
line = line.rstrip()
# 前两个参数是空字符串
line = line.translate(line.maketrans("", "", string.punctuation))
line = line.lower()
words = line.split()
for word in words:
if word not in counts:
counts[word] = 1
else:
counts[word] += 1
print(counts)
# 代码: https://www.py4e.com/code3/count2.py
学习“Python 艺术”或“Pythonic 思维”的一部分是认识到 Python 通常为许多常见的数据分析问题提供了内置功能。随着时间的推移,你会看到足够多的示例代码并阅读足够多的文档,从而知道去哪里查找是否有人已经编写了使你的工作更容易的东西。
以下是输出的缩略版本:
Enter the file name: romeo-full.txt
{'romeo': 40, 'and': 42, 'juliet': 32, 'act': 1, '2': 2, 'scene': 2,
'ii': 1, 'capulets': 1, 'orchard': 2, 'enter': 1, 'he': 5, 'jests': 1,
'at': 9, 'scars': 1, 'that': 30, 'never': 2, 'felt': 1, 'a': 24,
'wound': 1, 'appears': 1, 'above': 6, 'window': 2, 'but': 18,
'soft': 1, 'what': 11, 'light': 5, 'through': 2, 'yonder': 2,
'breaks': 1, ...}
浏览这个输出仍然很麻烦,我们可以使用 Python 来精确地得到我们正在寻找的东西,但要做到这一点,我们需要学习 Python 的元组。一旦我们学习了元组,我们将继续这个例子。
调试
当你处理更大的数据集时,通过打印和手动检查数据进行调试可能会变得很麻烦。以下是调试大型数据集的一些建议:
缩减输入规模 (Scale down the input)
如果可能,减小数据集的大小。例如,如果程序读取一个文本文件,从仅前 10 行开始,或者从你能找到的最小示例开始。你可以编辑文件本身,或者(更好的是)修改程序使其只读取前 n
行。
如果有错误,你可以将 n
减小到能体现错误的最小值,然后在找到并纠正错误时逐渐增加它。
检查摘要和类型 (Check summaries and types)
与其打印和检查整个数据集,不如考虑打印数据的摘要:例如,字典中的项数或数字列表的总和。
运行时错误的一个常见原因是值的类型不正确。对于调试这类错误,打印值的类型通常就足够了。
编写自检代码 (Write self-checks)
有时你可以编写代码来自动检查错误。例如,如果你正在计算一个数字列表的平均值,你可以检查结果是否不大于列表中的最大元素或小于最小元素。这被称为“健全性检查 (sanity check)”,因为它能检测到“完全不合逻辑”的结果。
另一种检查是将两个不同计算的结果进行比较,看它们是否一致。这被称为“一致性检查 (consistency check)”。
美化打印输出 (Pretty print the output)
格式化调试输出可以更容易地发现错误。
再次强调,你花在构建脚手架上的时间可以减少你花在调试上的时间。
术语表
字典 (dictionary) 从一组键到其对应值的映射。 哈希表 (hashtable) 用于实现 Python 字典的算法。 哈希函数 (hash function) 哈希表用于计算键位置的函数。 直方图 (histogram) 一组计数器。 实现 (implementation) 执行计算的一种方式。 项 (item) 键值对的另一个名称。 键 (key) 作为键值对第一部分出现在字典中的对象。 键值对 (key-value pair) 从键到值的映射的表示。 查找 (lookup) 接受一个键并找到相应值的字典操作。 嵌套循环 (nested loops) 当一个或多个循环“位于”另一个循环内部时。每次外层循环运行一次,内层循环都会运行到完成。 值 (value) 作为键值对第二部分出现在字典中的对象。这比我们之前使用的“值”一词更具体。
练习
练习 2: 编写一个程序,根据提交是在星期几对每封邮件消息进行分类。为此,查找以“From”开头的行,然后查找第三个单词,并对每周的每一天进行运行计数。在程序结束时打印出字典的内容(顺序无关紧要)。
示例行:
From [email protected] Sat Jan 5 09:14:16 2008
示例执行:
python dow.py
Enter a file name: mbox-short.txt
{'Fri': 20, 'Thu': 6, 'Sat': 1}
练习 3: 编写一个程序,通读邮件日志,使用字典构建一个直方图,以计算来自每个电子邮件地址的消息数量,并打印该字典。
Enter file name: mbox-short.txt
{'[email protected]': 1, '[email protected]': 3,
'[email protected]': 5, '[email protected]': 1,
'[email protected]': 2, '[email protected]': 3,
'[email protected]': 4, '[email protected]': 1,
'[email protected]': 4, '[email protected]': 2,
'[email protected]': 1}
练习 4: 向上述程序添加代码,以找出文件中谁的消息最多。在读取所有数据并创建字典后,使用最大值循环(参见第 5 章:最大值和最小值循环)遍历字典,找出谁的消息最多,并打印该人有多少条消息。
Enter a file name: mbox-short.txt
[email protected] 5
Enter a file name: mbox.txt
[email protected] 195
练习 5: 这个程序记录消息发送自的域名(而不是地址),而不是邮件来自谁(即完整的电子邮件地址)。在程序结束时,打印出字典的内容。
python schoolcount.py
Enter a file name: mbox-short.txt
{'media.berkeley.edu': 4, 'uct.ac.za': 6, 'umich.edu': 7,
'gmail.com': 1, 'caret.cam.ac.uk': 1, 'iupui.edu': 8}
- 如果你确实想了解更多关于哈希表的信息,https://www.cc4e.com 上有一门课程探讨了编程语言 C 如何实现 Python 字典。 ↩︎
如果你在本书中发现错误,欢迎使用 Github 给我发送修正。