列表
列表是一个序列
与字符串类似,列表 (list) 也是一个值的序列。在字符串中,值是字符;在列表中,它们可以是任何类型。列表中的值被称为元素 (elements),有时也称为项 (items)。
有几种创建新列表的方法;最简单的是将元素放在方括号(“[” 和 ”]”)中:
[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']
第一个例子是一个包含四个整数的列表。第二个是包含三个字符串的列表。列表的元素不必是同一类型。以下列表包含一个字符串、一个浮点数、一个整数和(瞧!)另一个列表:
['spam', 2.0, 5, [10, 20]]
一个列表嵌套在另一个列表中称为嵌套 (nested)。
不包含任何元素的列表称为空列表;你可以用空方括号 []
创建一个。
如你所料,你可以将列表值赋给变量:
>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> numbers = [17, 123]
>>> empty = []
>>> print(cheeses, numbers, empty)
['Cheddar', 'Edam', 'Gouda'] [17, 123] []
列表是可变的
访问列表元素的语法与访问字符串字符的语法相同:使用方括号运算符。方括号内的表达式指定索引。记住索引从 0 开始:
>>> print(cheeses[0])
Cheddar
与字符串不同,列表是可变的 (mutable),因为你可以更改列表中项目的顺序或重新分配列表中的项目。当方括号运算符出现在赋值语句的左侧时,它标识了将被赋值的列表元素。
>>> numbers = [17, 123]
>>> numbers[1] = 5
>>> print(numbers)
[17, 5]
numbers
的第 1 个元素(索引为 1),原来是 123,现在是 5。
你可以将列表视为索引和元素之间的一种关系。这种关系称为映射 (mapping);每个索引“映射到”一个元素。
列表索引的工作方式与字符串索引相同:
- 任何整数表达式都可以用作索引。
- 如果你尝试读取或写入不存在的元素,你会得到一个
IndexError
。 - 如果索引值为负数,它从列表末尾向前计数。
in
运算符也适用于列表。
>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> 'Edam' in cheeses
True
>>> 'Brie' in cheeses
False
遍历列表
遍历列表元素最常用的方法是使用 for
循环。语法与字符串相同:
for cheese in cheeses:
print(cheese)
如果你只需要读取列表的元素,这种方法效果很好。但是如果你想写入或更新元素,你需要索引。一种常用的方法是结合使用 range
和 len
函数:
for i in range(len(numbers)):
numbers[i] = numbers[i] * 2
这个循环遍历列表并更新每个元素。len
返回列表中的元素数量。range
返回一个从 0 到 n − 1 的索引列表,其中 n 是列表的长度。每次循环,i
都会获得下一个元素的索引。主体中的赋值语句使用 i
来读取元素的旧值并赋新值。
对空列表的 for
循环永远不会执行循环体:
for x in empty:
print('This never happens.')
虽然一个列表可以包含另一个列表,但嵌套的列表仍然算作单个元素。下面这个列表的长度是四:
['spam', 1, ['Brie', 'Roquefort', 'Pol le Veq'], [1, 2, 3]]
列表操作
+
运算符连接列表:
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> c = a + b
>>> print(c)
[1, 2, 3, 4, 5, 6]
类似地,*
运算符将列表重复指定的次数:
>>> [0] * 4
[0, 0, 0, 0]
>>> [1, 2, 3] * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]
第一个例子重复了四次。第二个例子将列表重复了三次。
列表切片
切片运算符也适用于列表:
>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> t[1:3]
['b', 'c']
>>> t[:4]
['a', 'b', 'c', 'd']
>>> t[3:]
['d', 'e', 'f']
如果你省略第一个索引,切片从开头开始。如果你省略第二个索引,切片到末尾结束。所以如果你两者都省略,切片是整个列表的副本。
>>> t[:]
['a', 'b', 'c', 'd', 'e', 'f']
由于列表是可变的,在执行会折叠、处理或损坏列表的操作之前,制作一个副本通常很有用。
赋值语句左侧的切片运算符可以更新多个元素:
>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> t[1:3] = ['x', 'y']
>>> print(t)
['a', 'x', 'y', 'd', 'e', 'f']
列表方法
Python 提供了操作列表的方法。例如,append
在列表末尾添加一个新元素:
>>> t = ['a', 'b', 'c']
>>> t.append('d')
>>> print(t)
['a', 'b', 'c', 'd']
extend
接受一个列表作为参数,并将其所有元素附加到原列表末尾:
>>> t1 = ['a', 'b', 'c']
>>> t2 = ['d', 'e']
>>> t1.extend(t2)
>>> print(t1)
['a', 'b', 'c', 'd', 'e']
这个例子保持 t2
不变。
sort
将列表的元素从低到高排列:
>>> t = ['d', 'c', 'e', 'b', 'a']
>>> t.sort()
>>> print(t)
['a', 'b', 'c', 'd', 'e']
大多数列表方法是无返回值的(void);它们修改列表并返回 None
。如果你不小心写了 t = t.sort()
,你会对结果感到失望。
删除元素
有几种方法可以从列表中删除元素。如果你知道想要删除元素的索引,可以使用 pop
:
>>> t = ['a', 'b', 'c']
>>> x = t.pop(1)
>>> print(t)
['a', 'c']
>>> print(x)
b
pop
修改列表并返回被移除的元素。如果你不提供索引,它会删除并返回最后一个元素。
如果你不需要被移除的值,可以使用 del
语句:
>>> t = ['a', 'b', 'c']
>>> del t[1]
>>> print(t)
['a', 'c']
如果你知道要移除的元素(但不知道索引),可以使用 remove
:
>>> t = ['a', 'b', 'c']
>>> t.remove('b')
>>> print(t)
['a', 'c']
remove
的返回值是 None
。
要移除多个元素,你可以使用带切片索引的 del
:
>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> del t[1:5]
>>> print(t)
['a', 'f']
像往常一样,切片选择所有元素,直到(但不包括)第二个索引。
列表和函数
有许多可以用于列表的内置函数,让你能够快速浏览列表而无需编写自己的循环:
>>> nums = [3, 41, 12, 9, 74, 15]
>>> print(len(nums))
6
>>> print(max(nums))
74
>>> print(min(nums))
3
>>> print(sum(nums))
154
>>> print(sum(nums)/len(nums))
25
sum()
函数仅在列表元素是数字时有效。其他函数(max()
、len()
等)适用于字符串列表和其他可比较的类型。
我们可以使用列表重写之前计算用户输入的数字列表平均值的程序。
首先,不使用列表计算平均值的程序:
total = 0
count = 0
while (True):
inp = input('Enter a number: ')
if inp == 'done': break
value = float(inp)
total = total + value
count = count + 1
average = total / count
print('Average:', average)
# 代码: https://www.py4e.com/code3/avenum.py
在这个程序中,我们有 count
和 total
变量来记录用户输入的数字的数量和运行总和,同时我们重复提示用户输入数字。
我们可以简单地记住用户输入的每个数字,并在最后使用内置函数计算总和和计数。
numlist = list()
while (True):
inp = input('Enter a number: ')
if inp == 'done': break
value = float(inp)
numlist.append(value)
average = sum(numlist) / len(numlist)
print('Average:', average)
# 代码: https://www.py4e.com/code3/avelist.py
我们在循环开始前创建一个空列表,然后每次得到一个数字时,就将其附加到列表中。在程序结束时,我们简单地计算列表中数字的总和,并将其除以列表中数字的数量,从而得出平均值。
列表和字符串
字符串是字符的序列,列表是值的序列,但字符列表与字符串不同。要将字符串转换为字符列表,你可以使用 list
:
>>> s = 'spam'
>>> t = list(s)
>>> print(t)
['s', 'p', 'a', 'm']
因为 list
是一个内置函数的名称,你应该避免将其用作变量名。我也避免使用字母“l”,因为它看起来太像数字“1”。所以我用“t”。
list
函数将字符串分解为单个字母。如果你想将字符串分解为单词,可以使用 split
方法:
>>> s = 'pining for the fjords'
>>> t = s.split()
>>> print(t)
['pining', 'for', 'the', 'fjords']
>>> print(t[2])
the
一旦你使用 split
将字符串分解为单词列表,你就可以使用索引运算符(方括号)来查看列表中的特定单词。
你可以调用 split
并传入一个可选参数,称为分隔符 (delimiter),它指定了用作单词边界的字符。以下示例使用连字符作为分隔符:
>>> s = 'spam-spam-spam'
>>> delimiter = '-'
>>> s.split(delimiter)
['spam', 'spam', 'spam']
join
是 split
的逆操作。它接受一个字符串列表并将元素连接起来。join
是一个字符串方法,所以你必须在分隔符上调用它,并将列表作为参数传递:
>>> t = ['pining', 'for', 'the', 'fjords']
>>> delimiter = ' '
>>> delimiter.join(t)
'pining for the fjords'
在这种情况下,分隔符是空格字符,所以 join
在单词之间放置一个空格。要连接没有空格的字符串,你可以使用空字符串 "" 作为分隔符。
解析行
通常,当我们读取文件时,我们想对行做些事情,而不仅仅是打印整行。我们常常想找到“有趣的行”,然后解析 (parse) 该行以找到行中一些有趣的部分 (part)。如果我们想从那些以“From”开头的行中打印出星期几该怎么办?
From [email protected] Sat Jan 5 09:14:16 2008
当面对这类问题时,split
方法非常有效。我们可以编写一个小程序,查找以“From”开头的行,split
这些行,然后打印出该行的第三个单词:
fhand = open('mbox-short.txt')
for line in fhand:
line = line.rstrip()
if not line.startswith('From '): continue
words = line.split()
print(words[2])
# 代码: https://www.py4e.com/code3/search5.py
该程序产生以下输出:
Sat
Fri
Fri
Fri
...
稍后,我们将学习越来越复杂的技术来挑选要处理的行,以及如何解析这些行以找到我们正在寻找的确切信息。
对象和值
如果我们执行这些赋值语句:
a = 'banana'
b = 'banana'
我们知道 a
和 b
都引用一个字符串,但我们不知道它们是否引用同一个字符串。有两种可能的状态:
变量和对象
一种情况是,a
和 b
引用两个具有相同值的不同对象。第二种情况是,它们引用同一个对象。
要检查两个变量是否引用同一个对象,你可以使用 is
运算符。
>>> a = 'banana'
>>> b = 'banana'
>>> a is b
True
在这个例子中,Python 只创建了一个字符串对象,a
和 b
都引用它。
但是当你创建两个列表时,你会得到两个对象:
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False
在这种情况下,我们会说这两个列表是等效的 (equivalent),因为它们有相同的元素,但不是同一的 (identical),因为它们不是同一个对象。如果两个对象是同一的,它们也是等效的,但如果它们是等效的,它们不一定是同一的。
到目前为止,我们一直互换使用“对象”和“值”,但更精确地说,一个对象有一个值。如果你执行 a = [1,2,3]
,a
引用一个列表对象,其值是一个特定的元素序列。如果另一个列表有相同的元素,我们会说它有相同的值。
别名
如果 a
引用一个对象,并且你赋值 b = a
,那么两个变量都引用同一个对象:
>>> a = [1, 2, 3]
>>> b = a
>>> b is a
True
变量与对象的关联称为引用 (reference)。在这个例子中,有两个对同一对象的引用。
一个有多个引用的对象有多个名称,所以我们说这个对象有别名 (aliased)。
如果带别名的对象是可变的,通过一个别名进行的更改会影响另一个别名:
>>> b[0] = 17
>>> print(a)
[17, 2, 3]
虽然这种行为可能很有用,但它容易出错。一般来说,当你处理可变对象时,避免使用别名更安全。
对于像字符串这样的不可变对象,别名问题不大。在这个例子中:
a = 'banana'
b = 'banana'
a
和 b
是否引用同一个字符串几乎没有任何区别。
列表参数
当你将列表传递给函数时,函数会获得对该列表的引用。如果函数修改了列表参数,调用者会看到这个变化。例如,delete_head
从列表中移除第一个元素:
def delete_head(t):
del t[0]
下面是它的用法:
>>> letters = ['a', 'b', 'c']
>>> delete_head(letters)
>>> print(letters)
['b', 'c']
参数 t
和变量 letters
是同一个对象的别名。
区分修改列表的操作和创建新列表的操作很重要。例如,append
方法修改列表,但 +
运算符创建新列表:
>>> t1 = [1, 2]
>>> t2 = t1.append(3)
>>> print(t1)
[1, 2, 3]
>>> print(t2)
None
>>> t3 = t1 + [3]
>>> print(t3)
[1, 2, 3]
>>> t1 is t3
False
当你编写旨在修改列表的函数时,这种差异很重要。例如,这个函数不会删除列表的头部:
def bad_delete_head(t):
t = t[1:] # 错误!
切片运算符创建了一个新列表,赋值使 t
引用它,但这一切对作为参数传递的列表没有任何影响。
另一种方法是编写一个创建并返回新列表的函数。例如,tail
返回列表中除第一个元素之外的所有元素:
def tail(t):
return t[1:]
这个函数保持原始列表不变。下面是它的用法:
>>> letters = ['a', 'b', 'c']
>>> rest = tail(letters)
>>> print(rest)
['b', 'c']
练习 1: 编写一个名为 chop
的函数,它接受一个列表并修改它,移除第一个和最后一个元素,并返回 None
。然后编写一个名为 middle
的函数,它接受一个列表并返回一个新列表,其中包含除第一个和最后一个元素之外的所有元素。
调试
不小心使用列表(以及其他可变对象)可能导致长时间的调试。以下是一些常见的陷阱和避免它们的方法:
不要忘记大多数列表方法会修改参数并返回
None
。这与字符串方法相反,后者返回一个新字符串并保持原始字符串不变。如果你习惯于这样编写字符串代码:
word = word.strip()
那么很容易写出这样的列表代码:
t = t.sort() # 错误!
因为
sort
返回None
,你接下来对t
执行的操作很可能会失败。在使用列表方法和运算符之前,你应该仔细阅读文档,然后在交互模式下测试它们。列表与其他序列(如字符串)共享的方法和运算符记录在:
docs.python.org/zh-cn/3/library/stdtypes.html#common-sequence-operations
仅适用于可变序列的方法和运算符记录在:
docs.python.org/zh-cn/3/library/stdtypes.html#mutable-sequence-types
选择一种习惯用法并坚持使用。
列表的部分问题在于做事的方式太多。例如,要从列表中移除一个元素,你可以使用
pop
、remove
、del
,甚至切片赋值。要添加一个元素,你可以使用
append
方法或+
运算符。但别忘了这些是正确的:t.append(x)
t = t + [x]而这些是错误的:
t.append([x]) # 错误!
t = t.append(x) # 错误!
t + [x] # 错误!
t = t + x # 错误!在交互模式下尝试这些例子,确保你理解它们的作用。注意只有最后一个会导致运行时错误;其他三个是合法的,但它们做了错误的事情。
创建副本来避免别名。
如果你想使用像
sort
这样会修改参数的方法,但又需要保留原始列表,你可以创建一个副本。orig = t[:]
t.sort()在这个例子中,你也可以使用内置函数
sorted
,它返回一个新的、已排序的列表,并保持原始列表不变。但在这种情况下,你应该避免使用sorted
作为变量名!列表、
split
和文件当我们读取和解析文件时,有很多机会遇到可能导致程序崩溃的输入,因此在编写通读文件并在“大海捞针”中寻找特定内容的程序时,重温守护模式是个好主意。
让我们重新审视我们那个在文件的 from 行中查找星期几的程序:
From [email protected] Sat Jan 5 09:14:16 2008
因为我们将这行分解成单词,我们可以省去使用
startswith
,只需查看行的第一个单词来确定我们是否对该行感兴趣。我们可以使用continue
来跳过第一个单词不是“From”的行,如下所示:fhand = open('mbox-short.txt')
for line in fhand:
words = line.split()
if words[0] != 'From' : continue
print(words[2])这看起来简单得多,我们甚至不需要做
rstrip
来移除文件末尾的换行符。但它更好吗?python search8.py
Sat
Traceback (most recent call last):
File "search8.py", line 5, in <module>
if words[0] != 'From' : continue
IndexError: list index out of range它某种程度上能工作,我们看到了第一行的星期几(Sat),但随后程序因回溯错误而失败。哪里出错了?是什么混乱的数据导致我们优雅、聪明且非常 Pythonic 的程序失败了?
你可以盯着它看很长时间并苦苦思索,或者向别人寻求帮助,但更快、更明智的方法是添加一个
print
语句。添加 print 语句的最佳位置是在程序失败的那一行之前,并打印出似乎导致失败的数据。现在这种方法可能会产生大量的输出行,但至少你会立即对问题有所了解。所以我们在第五行之前添加一个打印变量
words
的语句。我们甚至在行前添加了前缀“Debug:”,以便将常规输出与调试输出分开。for line in fhand:
words = line.split()
print('Debug:', words)
if words[0] != 'From' : continue
print(words[2])当我们运行程序时,大量的输出会滚出屏幕,但在最后,我们看到了我们的调试输出和回溯信息,所以我们知道在回溯之前发生了什么。
Debug: ['X-DSPAM-Confidence:', '0.8475']
Debug: ['X-DSPAM-Probability:', '0.0000']
Debug: []
Traceback (most recent call last):
File "search9.py", line 6, in <module>
if words[0] != 'From' : continue
IndexError: list index out of range每个调试行都打印出我们通过将行
split
成单词得到的单词列表。当程序失败时,单词列表是空的[]
。如果我们用文本编辑器打开文件并查看该文件,在那一点它看起来如下:X-DSPAM-Result: Innocent
X-DSPAM-Processed: Sat Jan 5 09:14:16 2008
X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772当我们的程序遇到空行时发生错误!空行上当然是“零个单词”。我们编写代码时为什么没有想到这一点?当代码查找第一个单词 (
word[0]
) 以检查它是否匹配“From”时,我们得到了“索引超出范围”的错误。这当然是添加一些守护代码以避免在第一个单词不存在时检查它的完美地方。有很多方法可以保护这段代码;我们将选择在查看第一个单词之前检查我们拥有的单词数量:
fhand = open('mbox-short.txt')
count = 0
for line in fhand:
words = line.split()
# print('Debug:', words)
if len(words) == 0 : continue
if words[0] != 'From' : continue
print(words[2])首先,我们注释掉了调试 print 语句而不是删除它,以防我们的修改失败并且需要再次调试。然后我们添加了一个守护语句,检查我们是否有零个单词,如果有,我们使用
continue
跳到文件中的下一行。我们可以将这两个
continue
语句视为帮助我们精炼对我们“感兴趣”并希望进一步处理的行的集合。一个没有单词的行对我们来说是“不有趣的”,所以我们跳到下一行。第一个单词不是“From”的行对我们来说是不有趣的,所以我们跳过它。修改后的程序成功运行,所以也许它是正确的。我们的守护语句确实确保了
words[0]
永远不会失败,但也许这还不够。在编程时,我们必须始终思考,“可能会出什么问题?”
练习 2: 找出上面程序中哪一行仍然没有得到适当的守护。看看你是否可以构造一个导致程序失败的文本文件,然后修改程序,使该行得到适当的守护,并对其进行测试,以确保它能处理你的新文本文件。
练习 3: 使用带有单个 if
语句的 or
逻辑运算符的复合逻辑表达式,重写上面示例中的守护代码,而不是使用两个 if
语句。
术语表
别名 (aliasing) 两个或多个变量引用同一个对象的情况。 分隔符 (delimiter) 用于指示字符串应在何处分割的字符或字符串。 元素 (element) 列表(或其他序列)中的值之一;也称为项。 等效的 (equivalent) 具有相同的值。 索引 (index) 指示列表中元素的整数值。 同一的 (identical) 是同一个对象(这意味着等效)。 列表 (list) 值的序列。 列表遍历 (list traversal) 按顺序访问列表中的每个元素。 嵌套列表 (nested list) 作为另一个列表元素的列表。 对象 (object) 变量可以引用的东西。对象具有类型和值。 引用 (reference) 变量与其值之间的关联。
练习
练习 4:找出文件中所有唯一的单词
莎士比亚在他的作品中使用了超过 20,000 个单词。但你将如何确定这一点?你将如何生成莎士比亚使用的所有单词的列表?你会下载他所有的作品,阅读它们,然后手动跟踪所有唯一的单词吗?
让我们改用 Python 来实现这一点。列出存储在文件 romeo.txt
(包含莎士比亚作品的一个子集)中所有按字母顺序排序的唯一单词。
首先,下载文件副本 www.py4e.com/code3/romeo.txt。创建一个将包含最终结果的唯一单词列表。编写一个程序打开文件 romeo.txt
并逐行读取。对于每一行,使用 split
函数将该行分割成单词列表。对于每个单词,检查该单词是否已在唯一单词列表中。如果该单词不在唯一单词列表中,则将其添加到列表中。程序完成后,按字母顺序排序并打印唯一单词列表。
Enter file: romeo.txt
['Arise', 'But', 'It', 'Juliet', 'Who', 'already',\
'and', 'breaks', 'east', 'envious', 'fair', 'grief',\
'is', 'kill', 'light', 'moon', 'pale', 'sick', 'soft',\
'sun', 'the', 'through', 'what', 'window',\
'with', 'yonder']
练习 5:极简电子邮件客户端
MBOX(邮箱)是一种流行的文件格式,用于存储和共享电子邮件集合。早期的电子邮件服务器和桌面应用程序使用这种格式。不涉及太多细节,MBOX 是一个文本文件,它连续存储电子邮件。电子邮件由以 From
开头的特殊行(注意空格)分隔。重要的是,以 From:
开头的行(注意冒号)描述电子邮件本身,并不起分隔符的作用。想象一下,你编写了一个极简的电子邮件应用程序,它列出用户收件箱中发件人的电子邮件地址并计算电子邮件的数量。
编写一个程序,通读邮箱数据,当你找到以“From”开头的行时,你将使用 split
函数将该行分割成单词。我们感兴趣的是谁发送了消息,也就是 From 行上的第二个单词。
From [email protected] Sat Jan 5 09:14:16 2008
你将解析 From 行并为每个 From 行打印出第二个单词,然后你还将计算 From(不是 From:)行的数量,并在最后打印出计数。这是一个很好的示例输出,其中删除了几行:
python fromcount.py
Enter a file name: mbox-short.txt
[email protected]
[email protected]
[email protected]
[...部分输出已移除...]
[email protected]
[email protected]
[email protected]
[email protected]
There were 27 lines in the file with From as the first word
练习 6:
重写提示用户输入数字列表并在用户输入“done”时最后打印出数字的最大值和最小值的程序。编写程序将用户输入的数字存储在列表中,并在循环完成后使用 max()
和 min()
函数计算最大值和最小值。
Enter a number: 6
Enter a number: 2
Enter a number: 9
Enter a number: 3
Enter a number: 5
Enter a number: done
Maximum: 9.0
Minimum: 2.0
如果你在本书中发现错误,欢迎使用 Github 给我发送修正。