Python for Everybody 中文版

Chapter 0 Base Material

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

0.11. PY4E - 面向所有人的 Python

切换导航

PY4E

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

0.11.1. 元组

0.11.1.1. 元组是不可变的

一个元组:sup:`1 <https://www.py4e.com/html3/10-tuples#fn1>`__` 类似于一个值的序列,就像列表一样。存储在元组中的值可以是任何类型,并由整数索引。重要的区别在于元组是*不可变*的。元组也是*可比较*和*可哈希*的,因此我们可以对元组列表进行排序,并在 Python 字典中将元组用作键值。

从语法上讲,元组是一个逗号分隔的值列表:

>>> t = 'a', 'b', 'c', 'd', 'e'

尽管并非必须,但通常将元组用括号括起来,以便我们在查看 Python 代码时能快速识别元组:

>>> t = ('a', 'b', 'c', 'd', 'e')

要创建一个包含单个元素的元组,必须包含最后的逗号:

>>> t1 = ('a',)
>>> type(t1)
<type 'tuple'>

在没有逗号的情况下,Python 将 ('a') 视为一个包含字符串的表达式,该字符串求值后得到一个字符串:

>>> t2 = ('a')
>>> type(t2)
<type 'str'>

构造元组的另一种方式是使用内置函数 tuple。不带参数时,它创建一个空元组:

>>> t = tuple()
>>> print(t)
()

如果参数是一个序列(字符串、列表或元组),则对 `tuple` 的调用结果是一个包含该序列元素的元组:

>>> t = tuple('lupins')
>>> print(t)
('l', 'u', 'p', 'i', 'n', 's')

因为 `tuple` 是构造函数的名称,所以应避免将其用作变量名。

大多数列表运算符也适用于元组。方括号运算符索引一个元素:

>>> t = ('a', 'b', 'c', 'd', 'e')
>>> print(t[0])
'a'

切片运算符选择元素范围。

>>> print(t[1:3])
('b', 'c')

但是,如果你尝试修改元组中的一个元素,你会得到一个错误:

>>> t[0] = 'A'
TypeError: object doesn't support item assignment

你不能修改元组的元素,但可以用另一个元组替换其中一个:

>>> t = ('A',) + t[1:]
>>> print(t)
('A', 'b', 'c', 'd', 'e')

0.11.1.2. 比较元组

比较运算符适用于元组和其他序列。Python 首先比较每个序列的第一个元素。如果它们相等,则继续比较下一个元素,依此类推,直到找到不同的元素。后续元素不会被考虑(即使它们非常大)。

>>> (0, 1, 2) < (0, 3, 4)
True
>>> (0, 1, 2000000) < (0, 3, 4)
True

sort 函数的作用方式相同。它主要按第一个元素排序,但在出现平局时,则按第二个元素排序,依此类推。

此特性适用于一种称为 DSU 的模式,用于

通过装饰序列构建一个元组列表,其中包含一个或多个排序键,这些键位于序列元素之前;使用 Python 内置的 sort 对该元组列表进行排序;并通过取消装饰提取序列的排序元素。

例如,假设你有一个单词列表,并希望按从长到短的顺序对其进行排序:

txt = 'but soft what light in yonder window breaks'
words = txt.split()
t = list()
for word in words:
    t.append((len(word), word))

t.sort(reverse=True)

res = list()
for length, word in t:
    res.append(word)

print(res)

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

第一个循环构建一个元组列表,其中每个元组由单词及其长度组成,单词位于长度之前。

sort 比较第一个元素、长度、第一个,且仅考虑第二个元素以打破平局。关键字参数 reverse=True 指示 sort 按降序排列。

第二个循环遍历元组列表,并构建一个按长度降序排列的单词列表。四个字符的单词按 反向 字母顺序排序,因此在以下列表中,“what”出现在“soft”之前。

程序的输出如下:

['yonder', 'window', 'breaks', 'light', 'what',
'soft', 'but', 'in']

当然,当该行被转换为 Python 列表并按单词长度降序排列时,其诗意效果会大打折扣。

0.11.1.3. 元组赋值

Python 语言独特的语法特性之一是在赋值语句的左侧使用元组。这允许在左侧为序列时一次性为多个变量赋值。

在此示例中,我们有一个包含两个元素的列表(即序列),并在单条语句中将序列的第一个和第二个元素分别赋给变量 x 和 y。

>>> m = [ 'have', 'fun' ]
>>> x, y = m
>>> x
'have'
>>> y
'fun'
>>>

它并非魔法,Python roughly 将元组赋值语法翻译为如下形式::sup:`2 <https://www.py4e.com/html3/10-tuples#fn2>`__

>>> m = [ 'have', 'fun' ]
>>> x = m[0]
>>> y = m[1]
>>> x
'have'
>>> y
'fun'
>>>

在风格上,当我们在赋值语句的左侧使用元组时,我们省略了括号,但以下语法同样有效:

>>> m = [ 'have', 'fun' ]
>>> (x, y) = m
>>> x
'have'
>>> y
'fun'
>>>

一个特别巧妙的元组赋值应用允许我们在一条语句中 交换 两个变量的值:

>>> a, b = b, a

该语句的两边都是元组,但左边是变量的元组,右边是表达式的元组。右边的每个值都被分配给左边相应的变量。右边的所有表达式在任何一个赋值之前都被求值。

左侧变量的数量必须与右侧值的数量相同:

>>> a, b = 1, 2, 3
ValueError: too many values to unpack

更一般地,右侧可以是任何类型的序列(字符串、列表或元组)。例如,要将电子邮件地址拆分为用户名和域名,你可以这样写:

>>> addr = 'monty@python.org'
>>> uname, domain = addr.split('@')

split 的返回值是一个包含两个元素的列表;第一个元素被赋值给 uname,第二个元素被赋值给 domain。

>>> print(uname)
monty
>>> print(domain)
python.org

0.11.1.4. 字典与元组

字典有一个名为 items 的方法,它返回一个元组列表,其中每个元组都是一个键值对:

>>> d = {'a':10, 'b':1, 'c':22}
>>> t = list(d.items())
>>> print(t)
[('b', 1), ('a', 10), ('c', 22)]

正如您从字典中预期的那样,项目没有特定的顺序。

然而,由于元组列表是一个列表,且元组是可比较的,我们现在可以对元组列表进行排序。将字典转换为元组列表是我们按键输出字典内容的一种方法:

>>> d = {'a':10, 'b':1, 'c':22}
>>> t = list(d.items())
>>> t
[('b', 1), ('a', 10), ('c', 22)]
>>> t.sort()
>>> t
[('a', 10), ('b', 1), ('c', 22)]

新列表按键值升序排列。

0.11.1.5. 使用字典进行多重赋值

通过组合 `items`、元组赋值以及 `for`,您可以看到一种在单个循环中遍历字典的键和值的良好代码模式:

for key, val in list(d.items()):
    print(val, key)

该循环有两个 迭代变量,因为 items 返回一个元组列表,而 key, val 是一个元组赋值,依次遍历字典中的每个键 - 值对。

对于循环中的每一次迭代,key 和 value 都会推进到字典中的下一个键 - 值对(仍按哈希顺序)。

该循环的输出为:

10 a
22 c
1 b

同样,它处于哈希键顺序(即,没有特定顺序)。

如果我们结合这两种技术,我们可以按每个键 - 值对中存储的 值 打印出字典的内容。

为此,我们首先创建一个元组列表,其中每个元组为 (value, key)。items 方法将给我们一个 (key, value) 元组列表,但这次我们希望按值而非键排序。一旦我们构建了包含值 - 键的元组列表,只需将列表按降序排序并打印出新排序后的列表即可。

>>> d = {'a':10, 'b':1, 'c':22}
>>> l = list()
>>> for key, val in d.items() :
...     l.append( (val, key) )
...
>>> l
[(10, 'a'), (22, 'c'), (1, 'b')]
>>> l.sort(reverse=True)
>>> l
[(22, 'c'), (10, 'a'), (1, 'b')]
>>>

通过仔细构建元组列表,使每个元组的第一个元素为值,我们可以对元组列表进行排序,从而按值对字典内容进行排序。

0.11.1.6. 最常见的单词

回到我们关于《罗密欧与朱丽叶》第二幕第二场文本的运行示例,我们可以增强我们的程序以使用此技术按如下方式打印文本中十个最常见的单词:

import string
fhand = open('romeo-full.txt')
counts = dict()
for line in fhand:
    line = line.translate(str.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

# Sort the dictionary by value
lst = list()
for key, val in list(counts.items()):
    lst.append((val, key))

lst.sort(reverse=True)

for key, val in lst[:10]:
    print(key, val)

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

程序的第一部分用于读取文件并计算将文档中的每个单词映射到其出现次数的字典,这部分保持不变。但与其简单地输出 counts 并结束程序,不如构造一个包含 (val, key) 个元组的列表,然后对该列表进行降序排序。

由于值在前,它将用于比较。如果有多个具有相同值的元组,它将查看第二个元素(键),因此值相同的元组将按键的字母顺序进一步排序。

在结尾,我们编写一个漂亮的 for 循环,它执行多重赋值迭代,并通过遍历列表的一个切片(lst[:10])打印出最常见的十个单词。

所以现在输出终于看起来符合我们对词频分析的需求。

61 i
42 and
40 romeo
34 to
34 the
32 thou
32 juliet
30 that
29 my
24 thee

这一复杂的数据解析和分析工作可以用一个易于理解的 19 行 Python 程序完成,这是 Python 成为探索信息的良好语言选择的原因之一。

0.11.1.7. 使用元组作为字典中的键

因为元组是 可哈希的 而列表不是,如果我们想创建一个 复合 键用于字典,我们必须使用元组作为键。

如果我们想要创建一个从姓氏和名字对映射到电话号码的电话簿,就会遇到复合键。假设我们已经定义了变量 last、first 和 number,我们可以按如下方式编写字典赋值语句:

directory[last,first] = number

方括号中的表达式是一个元组。我们可以在一个 `for` 循环中使用元组赋值来遍历该字典。

for last, first in directory:
    print(first, last, directory[last,first])

该循环遍历 directory 中的键,这些键是元组。它将每个元组的元素分别赋给 last 和 first,然后打印姓名和对应的电话号码。

0.11.1.8. 序列:字符串、列表和元组 - 天哪!

我专注于元组列表,但本章中的几乎所有示例也适用于列表的列表、元组的元组以及元组的列表。为了避免枚举所有可能的组合,有时谈论序列的序列会更容易。

在许多情况下,不同类型的序列(字符串、列表和元组)可以互换使用。那么您如何以及为何选择其中一种而不是其他?

首先显而易见的是,字符串比其他序列更受限,因为元素必须是字符。它们也是不可变的。如果你需要更改字符串中的字符(而不是创建新字符串)的能力,你可能想改用字符列表。

列表比元组更常见,主要是因为列表是可变的。但在以下几种情况下,你可能更倾向于使用元组:

  1. 在某些上下文中,例如 `return` 语句,创建元组比创建列表在语法上更简单。在其他上下文中,您可能更喜欢列表。

  2. 如果你想使用序列作为字典的键,你必须使用不可变类型,如元组或字符串。

  3. 如果您将序列作为参数传递给函数,使用元组可以减少因别名化而导致意外行为的可能性。

由于元组是不可变的,它们不提供像 `sort` 和 `reverse` 这样的方法来修改现有的列表。然而,Python 提供了内置函数 `sorted` 和 `reversed`,它们接受任意序列作为参数,并返回一个具有相同元素但顺序不同的新序列。

0.11.1.9. 列表推导式

有时您想通过使用来自另一个序列的数据来创建序列。您可以通过编写 for 循环并逐个追加项目来实现这一点。例如,如果您想将一组字符串(每个字符串存储数字)转换为可以求和的数字,您将编写如下代码:

list_of_ints_in_strings = ['42', '65', '12']
list_of_ints = []
for x in list_of_ints_in_strings:
    list_of_ints.append(int(x))

print(sum(list_of_ints))

使用列表推导式,上述代码可以以更紧凑的方式编写:

list_of_ints_in_strings = ['42', '65', '12']
list_of_ints = [ int(x) for x in list_of_ints_in_strings ]
print(sum(list_of_ints))

0.11.1.10. 调试

列表、字典和元组统称为 数据结构;在本章中,我们开始看到复合数据结构,如元组列表,以及将元组作为键、将列表作为值的字典。复合数据结构很有用,但它们容易引发我所说的 形状错误;也就是说,当数据结构具有错误的类型、大小或组成时引发的错误,或者也许你编写了一些代码并忘记了数据的形状从而引入了错误。例如,如果你期望一个包含一个整数的列表,而我给你一个普通的整数(不在列表中),它就无法工作。

0.11.1.11. 术语表

comparable

一种类型,其中一个值可以检查是否大于、小于或等于同一类型的另一个值。可比较的类型可以放入列表并进行排序。

data structure

相关值的集合,通常组织为列表、字典、元组等。

DSU

"decorate-sort-undecorate"的缩写,一种涉及构建元组列表、排序并提取部分结果的模式。

gather

组装可变长度参数元组的操作。

hashable

具有哈希函数的类型。整数、浮点数和字符串等不可变类型是可哈希的;列表和字典等可变类型则不是。

scatter

将序列视为参数列表进行操作。

shape (of a data structure)

数据结构类型、大小和组成的摘要。

singleton

仅包含单个元素的列表(或其他序列)。

tuple

元素的不可变序列。

tuple assignment

右侧为序列、左侧为变量元组的赋值操作。先对右侧求值,然后将其元素赋值给左侧的变量。

0.11.1.12. 练习

练习 1:修改之前的程序如下:读取并解析“From”行,从该行中提取地址。使用字典统计来自每个人的消息数量。

在读取完所有数据后,通过从字典中创建 (count, email) 元组列表来打印提交次数最多的人。然后按降序对该列表进行排序,并打印出提交次数最多的人。

Sample Line:
From stephen.marquard@uct.ac.za Sat Jan  5 09:14:16 2008

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

Enter a file name: mbox.txt
zqian@umich.edu 195

练习 2:本程序统计每天每个消息的小时分布。您可以通过查找时间字符串,然后使用冒号字符将该字符串拆分为部分,从而从“From”行提取小时。一旦您为每个小时累积了计数,就按小时排序打印出计数,每行一个,如下所示。

python timeofday.py
Enter a file name: mbox-short.txt
04 3
06 1
07 1
09 2
10 3
11 6
14 1
15 2
16 4
17 2
18 1
19 1

练习 3:编写一个程序,读取一个文件并按频率降序打印 *字母*。你的程序应将所有输入转换为小写,并且只统计字母 a-z。你的程序不应统计空格、数字、标点符号或除字母 a-z 以外的任何内容。从几种不同的语言中查找文本样本,并观察字母频率在不同语言之间的变化。将你的结果与 https://wikipedia.org/wiki/Letter_frequencies 中的表格进行比较。


  1. 趣味事实:单词“tuple”源自不同长度数字序列的名称:single, double, triple, quadruple, quintuple, sextuple, septuple 等。`↩︎ <https://www.py4e.com/html3/10-tuples#fnref1>`__

  2. Python 不会逐字翻译语法。例如,如果您尝试对字典使用此方法,它将不会按您预期的那样工作。 ↩︎


如果您发现本书中有错误,欢迎使用 Github 向我发送修正。

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

关闭窗口