Python for Everybody 中文版

Chapter 0 Base Material

| 关于   «  0.9. PY4E - Python for Everybody   ::   目录   ::   0.11. PY4E - 面向所有人的 Python  »

0.10. PY4E - 面向所有人的 Python

切换导航

PY4E

第 1 章:简介 第 2 章:变量 第 3 章:条件语句 第 4 章:函数 第 5 章:迭代 第 6 章:字符串 第 7 章:文件 第 8 章:列表 第 9 章:字典 第 10 章:元组 第 11 章:正则表达式 第 12 章:网络程序 第 13 章:Python 与 Web 服务 第 14 章:Python 对象 第 15 章:Python 与数据库 第 16 章:数据可视化

0.10.1. 字典

一个 字典 类似于列表,但更为通用。在列表中,索引位置必须是整数;而在字典中,索引可以是(几乎)任意类型。

你可以将字典视为一组索引(称为 键)与一组值之间的映射。每个键映射到一个值。键与值的关联称为 键 - 值对 或有时称为 项。

例如,我们将构建一个从英语到西班牙语单词的字典,因此键和值都是字符串。

函数 `dict` 创建一个没有任何项的新字典。因为 `dict` 是内置函数的名称,所以应避免将其用作变量名。

>>> eng2sp = dict()
>>> print(eng2sp)
{}

之前的花括号,{},代表一个空字典。要向字典中添加项,您可以使用方括号:

>>> eng2sp['one'] = 'uno'

上一行创建了一个从键 'one' 映射到值"uno"的项。如果我们再次打印字典,会看到一个键值对,键与值之间用冒号分隔:

>>> print(eng2sp)
{'one': 'uno'}

之前的输出格式也是一种输入格式。例如,你可以创建一个包含三个项的新字典。但是,如果你打印``eng2sp``,你可能会感到惊讶:

>>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
>>> print(eng2sp)
{'one': 'uno', 'three': 'tres', 'two': 'dos'}

键 - 值对的顺序并不相同。事实上,如果你在电脑上输入相同的示例,你可能会得到不同的结果。一般来说,字典中项目的顺序是不可预测的。

但这并不是一个问题,因为字典的元素从不使用整数索引进行索引。相反,您使用键来查找对应的值:

>>> 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 使用一种称为 哈希表 的算法,它具有一个显著特性:无论字典中有多少个元素,in 运算符所需的时间都大致相同。我不会解释为什么哈希函数如此神奇,但您可以在 wikipedia.org/wiki/Hash_table 上阅读更多相关内容。

练习 1:下载该文件的副本 www.py4e.com/code3/words.txt

编写一个程序,读取 words.txt 中的单词并将其作为键存储在字典中。值的含义无关紧要。之后,你可以使用 in 运算符作为快速检查字符串是否存在于字典中的方法。

0.10.1.1. 字典作为一组计数器

假设你被给定一个字符串,并且你想要计算每个字母出现的次数。有几种方法你可以这样做:

  1. 您可以创建 26 个变量,每个字母一个。然后遍历字符串,对于每个字符,递增相应的计数器,可能使用链式条件语句。

  2. 您可以创建一个包含 26 个元素的列表。然后将每个字符转换为数字(使用内置函数 ord),以该数字作为列表索引,并递增相应的计数器。

  3. 您可以创建一个字典,以字符为键,以计数器为对应的值。第一次看到某个字符时,您会向字典中添加一项。之后,您会递增现有项的值。

这些选项中的每一个都执行相同的计算,但每一个都以不同的方式实现该计算。

一种 实现 是执行计算的一种方法;有些实现比其他实现更好。例如,字典实现的一个优势是,我们不必事先知道字符串中出现哪些字母,只需为实际出现的字母预留空间即可。

以下是代码可能看起来的样子:

word = 'brontosaurus'
d = dict()
for c in word:
    if c not in d:
        d[c] = 1
    else:
        d[c] = d[c] + 1
print(d)

我们实际上是在计算一个 直方图,这是计数器(或频率)集合的统计学术语。

The for 循环遍历字符串。每次循环,如果字符 c 不在字典中,我们创建一个新项,其键为 c,初始值为 1(因为我们已经见过这个字母一次)。如果 c 已经在字典中,我们递增 d[c]。

这是程序的输出结果:

{'a': 1, 'b': 1, 'o': 2, 'n': 1, 's': 2, 'r': 2, 'u': 2, 't': 1}

直方图表明键"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 中非常常用的“惯用法”,我们将在本书的其余部分多次使用它。因此,你应该花一点时间,比较一下使用 if 语句和 in 操作符的循环与使用 get 方法 的循环。它们做完全相同的事情,但其中一个更简洁。

0.10.1.2. 字典与文件

字典的常见用途之一是统计文件中单词的出现次数。让我们从一个非常简单的单词文件开始,该文件取自《罗密欧与朱丽叶》的文本。

对于第一组示例,我们将使用一个缩短且简化版的文本,不含标点符号。稍后我们将处理包含标点的场景文本。

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 循环。外层循环正在读取文件中的行,而内层循环正在遍历该行中的每个单词。这是一个称为 嵌套循环 的模式示例,因为其中一个循环是 外层 循环,另一个循环是 内层 循环。

因为内循环每次外循环执行一次迭代时都会执行完所有迭代,所以我们认为内循环迭代得“更快”,而外循环迭代得更慢。

两个嵌套循环的结合确保了我们将统计输入文件中每一行的每个单词。

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)

# Code: http://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
{'and': 3, 'envious': 1, 'already': 1, 'fair': 1,
'is': 3, 'through': 1, 'pale': 1, 'yonder': 1,
'what': 1, 'sun': 2, 'Who': 1, 'But': 1, 'moon': 1,
'window': 1, 'sick': 1, 'east': 1, 'breaks': 1,
'grief': 1, 'with': 1, 'light': 1, 'It': 1, 'Arise': 1,
'kill': 1, 'the': 3, 'soft': 1, 'Juliet': 1}

查阅字典以查找最常见的键及其计数略显不便,因此我们需要添加一些额外的 Python 代码来获取更有用的输出。

0.10.1.3. 循环与字典

如果使用字典作为 `for` 语句中的序列,它将遍历字典的键。该循环打印每个键及其对应的值:

counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
for key in counts:
    print(key, counts[key])

这就是输出看起来的样子:

jan 100
chuck 1
annie 42

同样,键没有特定的顺序。

我们可以使用此模式来实现前面描述的各类循环惯用法。例如,如果我们想查找字典中所有值大于十的条目,我们可以编写如下代码:

counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
for key in counts:
    if counts[key] > 10 :
        print(key, counts[key])

之前的``for``循环遍历字典的 键,因此我们必须使用索引运算符来获取每个键对应的 值。以下是输出结果的样子:

jan 100
annie 42

我们仅看到值大于 10 的条目。

如果您想按字母顺序打印键,您首先使用字典对象中可用的 keys 方法创建字典中键的列表,然后对该列表进行排序并遍历该排序后的列表,查找每个键并按如下方式以排序顺序打印出键 - 值对:

counts = { 'chuck' : 1 , 'annie' : 42, 'jan': 100}
lst = list(counts.keys())
print(lst)
lst.sort()
for key in lst:
    print(key, counts[key])

这就是输出看起来的样子:

['jan', 'chuck', 'annie']
annie 42
chuck 1
jan 100

首先,您看到的是以未排序顺序从 keys 方法获取的键列表。然后,我们看到了从 for 循环中按顺序获取的键 - 值对。

0.10.1.4. 高级文本解析

在上面的示例中,使用文件 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))

用 ``tostr`` 中相同位置的字符替换 ``fromstr`` 中的字符,并删除所有在 ``deletestr`` 中的字符。``fromstr`` 和 ``tostr`` 可以是空字符串,且可以省略 ``deletestr`` 参数。

我们不会指定 tostr,但我们会使用 deletestr 参数来删除所有标点符号。我们甚至让 Python 告诉我们它认为哪些字符是“标点符号”:

>>> import string
>>> string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

之前使用的参数在 Python 2.0 中与 translate 不同。

我们对程序进行如下修改:

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)

# Code: http://www.py4e.com/code3/count2.py

学习“Python 艺术”或“以 Python 方式思考”的一部分在于认识到 Python 通常对许多常见数据分析问题具有内置功能。随着时间的推移,您将看到足够多的示例代码并阅读足够多的文档,从而知道在哪里查找,以查看是否有人已经编写了使您的工作轻松得多的内容。

以下为输出的缩写版本:

Enter the file name: romeo-full.txt
{'swearst': 1, 'all': 6, 'afeard': 1, 'leave': 2, 'these': 2,
'kinsmen': 2, 'what': 11, 'thinkst': 1, 'love': 24, 'cloak': 1,
a': 24, 'orchard': 2, 'light': 5, 'lovers': 2, 'romeo': 40,
'maiden': 1, 'whiteupturned': 1, 'juliet': 32, 'gentleman': 1,
'it': 22, 'leans': 1, 'canst': 1, 'having': 1, ...}

浏览此输出仍然繁琐,我们可以使用 Python 为我们提供确切所需的内容,但为此,我们需要了解 Python 元组。我们将在了解元组后再次使用此示例。

0.10.1.5. 调试

随着您处理的数据集规模增大,通过打印和手动检查数据来进行调试会变得难以管理。以下是针对调试大型数据集的一些建议:

缩小输入规模 如果可能,请减小数据集的大小。例如,如果程序读取文本文件,请仅从前十行开始,或者从你能找到的最小示例开始。你可以直接编辑文件本身,或者(更佳)修改程序,使其仅读取前 n 行。

如果存在错误,您可以将 `n` 减小到能显现该错误的最小值,然后随着您发现并纠正错误而逐渐增加它。

检查摘要和类型 与其打印并检查整个数据集,不如打印数据的摘要:例如,字典中的项目数量或数字列表的总和。

运行时错误的一个常见原因是值不是正确的类型。对于调试这类错误,通常只需打印值的类型即可。

编写自检代码 有时你可以编写代码来自动检查错误。例如,如果你正在计算一组数字的平均值,你可以检查结果不大于列表中的最大元素,也不小于最小元素。这被称为“合理性检查”,因为它能检测到“完全不合逻辑”的结果。

另一种检查方法比较两种不同计算的结果,以查看它们是否一致。这被称为“一致性检查”。

美化打印输出 格式化调试输出可以更容易地发现错误。

再次,你花在搭建脚手架上的时间可以减少你花在调试上的时间。

0.10.1.6. 术语表

dictionary

从一组键到其对应值的映射。

hashtable

用于实现 Python 字典的算法。

hash function

哈希表用来计算键位置的函数。

histogram

一组计数器。

implementation

一种执行计算的方法。

item

键值对的另一种名称。

key

在字典中作为键值对第一部分出现的对象。

key-value pair

从键到值的映射的表示。

lookup

一种字典操作,它接受一个键并查找对应的值。

nested loops

当一个或多个循环位于另一个循环“内部”时。外部循环每运行一次,内部循环都会运行至完成。

value

在字典中作为键值对第二部分出现的对象。这比我们之前使用的“值”一词更具体。

0.10.1.7. 练习

练习 2:编写一个程序,将每封邮件消息按提交日期所在的星期几进行分类。为此,查找以 "From" 开头的行,然后查找第三个单词,并持续统计每个星期的数量。在程序结束时,打印出字典的内容(顺序无关紧要)。

示例行:

From stephen.marquard@uct.ac.za 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
{'gopal.ramasammycook@gmail.com': 1, 'louis@media.berkeley.edu': 3,
'cwen@iupui.edu': 5, 'antranig@caret.cam.ac.uk': 1,
'rjlowe@iupui.edu': 2, 'gsilver@umich.edu': 3,
'david.horwitz@uct.ac.za': 4, 'wagnermr@iupui.edu': 1,
'zqian@umich.edu': 4, 'stephen.marquard@uct.ac.za': 2,
'ray@media.berkeley.edu': 1}

练习 4:在上面的程序中增加代码,以确定谁在文件中的消息最多。在所有数据都已读取且字典已创建之后,使用最大循环(参见第 5 章:最大和最小循环)遍历字典,找出谁的消息最多,并打印该人有多少条消息。

Enter a file name: mbox-short.txt
cwen@iupui.edu 5

Enter a file name: mbox.txt
zqian@umich.edu 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}

如果您在本书中发现错误,请随时使用 Github 向我发送修正。

   «  0.9. PY4E - Python for Everybody   ::   目录   ::   0.11. PY4E - 面向所有人的 Python  »

关闭窗口