Skip to main content

元组

元组是不可变的

元组 1 是一个值的序列,非常像列表。存储在元组中的值可以是任何类型,并且它们由整数索引。重要的区别在于元组是不可变的 (immutable)。元组也是可比较的 (comparable) 和可哈希的 (hashable),因此我们可以对包含元组的列表进行排序,并使用元组作为 Python 字典中的键值。

在语法上,元组是用逗号分隔的值列表:

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

虽然不是必需的,但通常将元组括在圆括号中,以帮助我们在查看 Python 代码时快速识别元组:

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

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

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

没有逗号,Python 会将 ('a') 视为一个包含字符串的表达式,其计算结果是一个字符串:

>>> t2 = ('a')
>>> type(t2)
<class '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')

比较元组

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

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

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

这个特性适用于一种称为 DSU 的模式,代表:

Decorate (修饰) 通过构建一个元组列表来修饰一个序列,其中每个元组包含一个或多个排序键,后面跟着来自序列的元素, Sort (排序) 使用 Python 内置的 sort 对元组列表进行排序,以及 Undecorate (还原) 通过提取序列的已排序元素。

例如,假设你有一个单词列表,并且你想将它们从长到短排序:

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)

# 代码: https://www.py4e.com/code3/soft.py

第一个循环构建了一个元组列表,其中每个元组是一个单词,前面是它的长度。

sort 首先比较第一个元素(长度),只有在比较第一个元素结果相同时才考虑第二个元素来打破平局。关键字参数 reverse=True 告诉 sort 按降序排列。

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

程序的输出如下:

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

当然,当这行诗变成一个 Python 列表并按单词长度降序排序时,它失去了很多诗意。

元组赋值

Python 语言独特的语法特性之一是能够在赋值语句的左侧使用元组,右侧使用序列。这允许你一次将多个变量赋给给定的序列。

在这个例子中,我们有一个包含两个元素的元组,并在一个语句中将元组的第一个和第二个元素赋给变量 xy

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

这比元组到元组的赋值更通用。元组和列表都是序列,所以这种语法也适用于包含两个元素的列表。

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

这不是魔法,Python 大致将元组赋值语法翻译成以下形式: 2

>>> 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 = '[email protected]'
>>> uname, domain = addr.split('@')

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

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

字典和元组

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

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

正如你对字典所期望的那样,这些项是非字母顺序的。

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

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

新的列表按键值的升序字母顺序排序。

使用字典进行多重赋值

结合 items、元组赋值和 for,你可以看到一个很好的代码模式,用于在单个循环中遍历字典的键和值:

d = {'a':10, 'b':1, 'c':22}
for key, val in d.items():
print(val, key)

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

对于循环的每次迭代,keyval 都前进到字典中的下一个键值对(仍然是哈希顺序)。

这个循环的输出是:

10 a
1 b
22 c

再次强调,它是按哈希键顺序(即没有特定顺序)。

如果我们将这两种技术结合起来,我们可以按存储在每个键值对中的对字典内容进行排序并打印出来。

为此,我们首先创建一个元组列表,其中每个元组是 (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'), (1, 'b'), (22, 'c')]
>>> l.sort(reverse=True)
>>> l
[(22, 'c'), (10, 'a'), (1, 'b')]
>>>

通过精心构造元组列表,使每个元组的值作为第一个元素,我们可以对元组列表进行排序,并按值对字典内容进行排序。

最常见的单词

回到我们正在进行的《罗密欧与朱丽叶》第二幕第二场文本的例子,我们可以增强我们的程序,使用这种技术打印出文本中最常见的十个单词,如下所示:

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

# 按值对字典排序
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)

# 代码: https://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 成为探索信息的良好语言选择的原因之一。

使用元组作为字典的键

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

如果我们想创建一个将姓氏、名字对映射到电话号码的电话簿,我们就会遇到复合键。假设我们已经定义了变量 lastfirstnumber,我们可以像下面这样编写一个字典赋值语句:

directory[last,first] = number

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

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

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

序列:字符串、列表和元组——哦,我的天!

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

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

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

列表比元组更常用,主要是因为它们是可变的。但在少数情况下,你可能更喜欢元组:

  1. 在某些上下文中,比如 return 语句,创建元组在语法上比创建列表更简单。在其他上下文中,你可能更喜欢列表。
  2. 如果你想使用一个序列作为字典键,你必须使用像元组或字符串这样的不可变类型。
  3. 如果你将一个序列作为参数传递给函数,使用元组可以减少由于别名引起的意外行为的可能性。

因为元组是不可变的,它们不提供像 sortreverse 这样修改现有列表的方法。然而,Python 提供了内置函数 sortedreversed,它们接受任何序列作为参数,并返回一个包含相同元素但顺序不同的新序列。

列表推导式

有时你想通过使用来自另一个序列的数据来创建一个序列。你可以通过编写 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 comprehension),上面的代码可以写得更紧凑:

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))

调试

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

术语表

可比较的 (comparable) 一种类型,其一个值可以与同一类型的另一个值进行比较,以判断大于、小于或等于。可比较的类型可以放入列表并排序。 数据结构 (data structure) 相关值的集合,通常组织在列表、字典、元组等中。 DSU “decorate-sort-undecorate”(修饰-排序-还原)的缩写,一种涉及构建元组列表、排序和提取部分结果的模式。 收集 (gather) 组装可变长度参数元组的操作。 可哈希的 (hashable) 具有哈希函数的类型。像整数、浮点数和字符串这样的不可变类型是可哈希的;像列表和字典这样的可变类型则不是。 散开 (scatter) 将序列视为参数列表的操作。 形状 (shape)(数据结构的) 数据结构类型、大小和组成的摘要。 单元素列表 (singleton) 包含单个元素的列表(或其他序列)。 元组 (tuple) 元素的不可变序列。 元组赋值 (tuple assignment) 右侧是序列,左侧是变量元组的赋值。右侧被求值,然后其元素被赋给左侧的变量。

练习

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

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

示例行:
From [email protected] Sat Jan 5 09:14:16 2008

输入文件名:mbox-short.txt
[email protected] 5

输入文件名:mbox.txt
[email protected] 195

练习 2: 这个程序统计每封邮件一天中哪个小时发送的分布情况。你可以通过查找时间字符串,然后使用冒号字符将该字符串分割成部分,从而从“From”行中提取小时。一旦你累积了每个小时的计数,按小时排序打印出计数,每行一个,如下所示。

python timeofday.py
输入文件名: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. 有趣的事实:“元组”这个词来自给不同长度数字序列的名称:single, double, triple, quadruple, quintuple, sextuple, septuple 等。 ↩︎
  2. Python 并不会字面上翻译这个语法。例如,如果你尝试对字典这样做,它可能不会像你期望的那样工作。 ↩︎

如果你在本书中发现错误,欢迎使用 Github 给我发送修正。