第十章:列表
本章介绍Python中最有用的内置类型之一:列表(list)。你还将进一步学习关于对象的知识 以及同一个对象拥有多个名称时会发生什么。
列表是一个序列
与字符串类似,列表 是由多个值组成的序列。在字符串中,每个值都是字符; 在列表中,值可以是任何数据类型。列表中的值称为 元素(element) ,有时也被称为 项(item) 。
创建新列表的方法有多种;最简单的方法是用方括号( [
和 ]
)将元素包括起来:
[10, 20, 30, 40] ['crunchy frog', 'ram bladder', 'lark vomit']
第一个例子是包含4个整数的列表。第二个是一个包含3个字符串的列表。 一个列表中的元素不需要是相同的数据类型。下面的列表包含一个字符串、一个浮点数、一个整数和另一个列表:
['spam', 2.0, 5, [10, 20]]
一个列表在另一个列表中,称为**嵌套(nested)列表**。
一个不包含元素的列表被称为空列表;你可以用空的方括号 []
创建一个空列表。
正如你想的那样,你可以将列表的值赋给变量:
>>> cheeses = ['Cheddar', 'Edam', 'Gouda'] >>> numbers = [42, 123] >>> empty = [] >>> print(cheeses, numbers, empty) ['Cheddar', 'Edam', 'Gouda'] [42, 123] []
列表是可变的
访问列表中元素的语法,与访问字符串中字符的语法相同,都是通过方括号运算符实现的。 括号中的表达式指定了元素的索引。记住,索引从0开始:
>>> cheeses[0] 'Cheddar'
和字符串不同的是,列表是可变的。当括号运算符出现在赋值语句的左边时,它就指向了列表中将被赋值的元素。
>>> numbers = [42, 123] >>> numbers[1] = 5 >>> numbers [42, 5]
numbers
中索引为1的元素,原来是123,现在变成了5。
图10-1:状态图 是 cheeses
、 nubmers
和 empty
的状态图。
列表用外部标有”list”的盒子表示,盒子内部是列表的元素。 cheeses
指向一个有3个元素的列表,3个元素的下标分别是0、1、2。numbers
包含两个元素; 状态图显示第二个元素原来是123,被重新赋值为5。 empty
对应一个没有元素的列表。
列表下标的工作原理和字符串下标相同:
- 任何整数表达式都可以用作下标。
- 如果你试图读或写一个不存在的元素,你将会得到一个索引错误(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 []: print('This never happens.')
尽管一个列表可以包含另一个列表,嵌套的列表本身还是被看作一个单个元素。 下面这个列表的长度是4:
['spam', 1, ['Brie', 'Roquefort', 'Pol le Veq'], [1, 2, 3]]
列表操作
+运算符拼接多个列表:
>>> a = [1, 2, 3] >>> b = [4, 5, 6] >>> c = a + b >>> 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]
第一个例子重复4次。第二个例子重复了那个列表3次。
列表切片
切片(slice)运算符同样适用于对列表:
>>> 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'] >>> t ['a', 'x', 'y', 'd', 'e', 'f']
列表方法
Python为列表提供了一些方法. 例如, append
添加一个新元素到列表的末端:
>>> t = ['a', 'b', 'c'] >>> t.append('d') >>> t ['a', 'b', 'c', 'd']
extend
将接受一个列表作为参数,并将其其中的所有元素添加至目标列表中:
>>> t1 = ['a', 'b', 'c'] >>> t2 = ['d', 'e'] >>> t1.extend(t2) >>> t1 ['a', 'b', 'c', 'd', 'e']
这个例子中 t2
没有改动。
sort
将列表中的元素从小到大进行排序:
>>> t = ['d', 'c', 'e', 'b', 'a'] >>> t.sort() >>> t ['a', 'b', 'c', 'd', 'e']
大部分的列表方法都是无返回值的;它们对列表进行修改,然后返回None。 如果你意外的写了 t.sort()
,你将会对结果感到失望的。
映射、筛选和归并
你可以这样使用循环,对列表中所有元素求和:
def add_all(t): total = 0 for x in t: total += x return total
total
被初始化为 0。每次循环时, x
从列表中获取一个元素。 运算符 += 提供了一个快捷的更新变量的方法。这个 增量赋值语句(augmented assignment statement)
total += x
等价于
total = total + x
当循环执行时,total
将累计元素的和;一个这样的变量有时被称为 累加器(accumulator) 。
把一个列表中的元素加起来是一个很常用的操作, 所以Python将其设置为一个内建内置函数 sum
:
>>> t = [1, 2, 3] >>> sum(t) 6
一个像这样的将一系列的元素合并成一个单一值的操作有时称为 归并(reduce) 。
有时,你在构建一个列表时还需要遍历另一个列表。 例如,下面的函数接受一个字符串列表作为参数,返回包含大写字符的新列表:
def capitalize_all(t): res = [] for s in t: res.append(s.capitalize()) return res
res
被初始化为一个空列表;每次循环时,我们添加下一个元素。 所以 res
是另一种形式的累加器。
类似 capitalize_all
这样的操作有时被称为 映射(map) ,因为它“映射”一个函数(在本例中是方法 capitalize
)到序列中的每个元素上。
另一个常见的操作是从列表中选择一些元素,并返回一个子列表。例如,下面的函数读取一个字符串列表,并返回一个仅包含大写字符串的列表:
def only_upper(t): res = [] for s in t: if s.isupper(): res.append(s) return res
isupper
是一个字符串方法,如果字符串仅含有大写字母,则返回 True
。
类似 only_upper
这样的操作被称为 筛选(filter) ,因为它选中某些元素,然后剔除剩余的元素。
大部分常用列表操作可以用映射、筛选和归并这个组合表示。
删除元素
有多种方法可以从列表中删除一个元素。如果你知道元素的下标,你可以使用 pop
:
>>> t = ['a', 'b', 'c'] >>> x = t.pop(1) >>> t ['a', 'c'] >>> x 'b'
pop
修改列表,并返回被移除的元素。如果你不提供下标,它将移除并返回最后一个元素。
如果你不需要被移除的元素,可以使用 del
运算符:
>>> t = ['a', 'b', 'c'] >>> del t[1] >>> t ['a', 'c']
如果你知道要删除的值(但是不知道其下标),你可以使用 remove
:
>>> t = ['a', 'b', 'c'] >>> t.remove('b') >>> t ['a', 'c']
remove
的返回值是None.
要移除多个元素,你可以结合切片索引使用 del
:
>>> t = ['a', 'b', 'c', 'd', 'e', 'f'] >>> del t[1:5] >>> t ['a', 'f']
同样的,切片选择到第二个下标(不包含第二个下标)处的所有元素。
列表和字符串
一个字符串是多个字符组成的序列,一个列表是多个值组成的序列。但是一个由字符组成的列表不同于字符串。可以使用 list
将一个字符串转换为字符的列表:
>>> s = 'spam' >>> t = list(s) >>> t ['s', 'p', 'a', 'm']
由于 list
是内置函数的名称,你应避免将它用作变量名。我同样避免使用 l
,因为它看起来很像1。这就是为什么我用了 t
。
list
函数将字符串分割成单独的字符。如果你想将一个字符串分割成一些单词,你可以使用 split
方法:
>>> s = 'pining for the fjords' >>> t = s.split() >>> t ['pining', 'for', 'the', 'fjords']
可以提高一个叫做 分隔符(delimiter) 的可选参数,指定什么字符作为单词之间的分界线。下面的例子使用连字符作为分隔符:
>>> s = 'spam-spam-spam' >>> delimiter = '-' >>> t = s.split(delimiter) >>> t ['spam', 'spam', 'spam']
join
的功能和 split
相反。它将一个字符串列表的元素拼接起来。join
是一个字符串方法,所以你需要在一个分隔符上调用它,并传入一个列表作为参数:
>>> t = ['pining', 'for', 'the', 'fjords'] >>> delimiter = ' ' >>> s = delimiter.join(t) >>> s 'pining for the fjords'
在这个例子中,分隔符是一个空格,所以 join
在单词之间添加一个空格。如果不使用空格拼接字符串,你可以使用空字符串 ''
作为分隔符。
对象和值
如果我们执行下面的赋值语句:
a = 'banana' b = 'banana'
我们知道 a
和 b
都指向一个字符串,但是我们不知道是否他们指向 同一个 字符串。这里有两种可能的状态,如图10-2:状态图所示。
一种情况是,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
所以状态图如图10-3:状态图所示。
在这个例子中,我们称这两个列表是 相等(equivalent) 的,因为它们有相同的元素。但它们并不 相同(identical) ,因为他们不是同一个对象。如果两个对象 相同 ,它们也是相等的,但是如果它们是相等的,它们不一定是相同的。
之前,我们一直在等价地使用”对象”和“值”,但是更准确的说,一个对象拥有一个值。 如果你对 [1, 2, 3]
求值,会得到一个值为整数序列的列表对象。 如果另一个列表有同样的元素,我们说它们有相同的值,但是它们并不是同一个对象。
别名
如果 a
指向一个对象,然后你赋值 b = a
,那么两个变量指向同一个对象:
>>> a = [1, 2, 3] >>> b = a >>> b is a True
状态图如图10-4:状态图所示。
变量和对象之间的关联称为 引用(reference) 。 在这个例子中,有两个对同一个对象的引用。
如果一个对象有多于一个引用,那它也会有多个名称, 我们称这个对象是 有别名的(aliased) 。
如果一个有别名的对象是可变的,对其中一个别名(alias)的改变对影响到其它的别名:
>>> b[0] = 42 >>> a [42, 2, 3]
尽管这个行为很有用,但是容易导致出现错误。 通常,避免对于可变对象使用别名相对更安全。
对于像字符串这样的不可变对象,使用别名没有什么问题。例如:
a = 'banana' b = 'banana'
a
和 b
是否指向同一个字符串基本上没有什么影响。
列表参数
当你将一个列表作为参数传给一个函数,函数将得到这个列表的一个引用。如果函数对这个列表进行了修改,会在调用者中有所体现。例如, delete_head
删除列表的第一个元素:
def delete_head(t): del t[0]
这样使用这个函数:
>>> letters = ['a', 'b', 'c'] >>> delete_head(letters) >>> letters ['b', 'c']
参数 t
和变量 letters
是同一个对象的别名。 其堆栈图如图10-5:堆栈图所示。
由于列表被两个帧共享,我把它画在它们中间。
需要注意的是修改列表操作和创建列表操作间的区别。 例如,append
方法是修改一个列表,而 + 运算符是创建一个新的列表:
>>> t1 = [1, 2] >>> t2 = t1.append(3) >>> t1 [1, 2, 3] >>> t2 None
append
修改列表并返回None。
>>> t3 = t1 + [4] >>> t1 [1, 2, 3] >>> t3 [1, 2, 3, 4] >>> t1
运算符 + 创建了一个新列表,而不改变原始的列表。
如果你要编写一个修改列表的函数,这一点就很重要。 例如,这个函数 不会 删除列表的第一个元素:
def bad_delete_head(t): t = t[1:] # 错的!
切片运算符创建了一个新列表,然后这个表达式让 t
指向了它, 但是并不会影响原来被调用的列表。
>>> t4 = [1, 2, 3] >>> bad_delete_head(t4) >>> t4 [1, 2, 3]
在 bad_delete_head
的开始处,t
和 t4
指向同一个列表。在结束时,t
指向一个新列表,但是 t4
仍然指向原来的、没有被改动的列表。
一个替代的写法是,写一个创建并返回一个新列表的函数。 例如,tail
返回列表中除了第一个之外的所有元素:
def tail(t): return t[1:]
这个函数不会修改原来的列表。下面是函数的使用方法:
>>> letters = ['a', 'b', 'c'] >>> rest = tail(letters) >>> rest ['b', 'c']
调试
粗心地使用列表(以及其他可变对象)会导致长时间的调试。 下面列举一些常见的陷阱以及避免它们的方法:
大多数的列表方法会对参数进行修改,然后返回
None
。这和字符串方法相反,后者保留原始的字符串并返回一个新的字符串。如果你习惯这样写字符串代码:
word = word.strip()
那么你很可能会写出下面的列表代码:
t = t.sort() # 错误!
因为
sort
返回None
,所以你的下一个对t
执行的操作很可能会失败。在使用
list
方法和操作符之前,你应该仔细阅读文档,然后在交互模式下测试。选择一种写法,坚持下去。
列表的一个问题就是有太多方法可以做同样的事情。 例如,要删除列表中的一个元素,你可以使用
pop
、remove
、del
甚至是切片赋值。要添加一个元素,你可以使用
append
方法或者 + 运算符。假设t
是一个列表,x
是一个列表元素,以下这些写法都是正确的:t.append(x) t = t + [x] t += [x]
而这些是错误的:
t.append([x]) # 错误! t = t.append(x) # 错误! t + [x] # 错误! t = t + x # 错误!
在交互模式下尝试每一个例子,保证你明白它们做了什么。 注意只有最后一个会导致运行时错误;其他的都是合乎规范的的,但结果却是错的。
通过创建拷贝来避免别名.
如果你要使用类似
sort
这样的方法来修改参数, 但同时有要保留原列表,你可以创建一个拷贝。>>> t = [3, 1, 2] >>> t2 = t[:] >>> t2.sort() >>> t [3, 1, 2] >>> t2 [1, 2, 3]
在这个例子中,你还可以使用内置函数
sorted
,它将返回一个新的已排序的列表,原列表将保持不变。>>> t2 = sorted(t) >>> t [3, 1, 2] >>> t2 [1, 2, 3]
术语表
列表(list):
多个值组成的序列。
元素(element):
列表(或序列)中的一个值,也称为项。
嵌套列表(nested list):
作为另一个列表的元素的列表。
累加器(accumulator):
循环中用于相加或累积出一个结果的变量。
增量赋值语句(augmented assignment):
一个使用类似 +=
操作符来更新一个变量的值的语句。
归并(reduce):
遍历序列,将所有元素求和为一个值的处理模式。
映射(map):
遍历序列,对每个元素执行操作的处理模式。
筛选(filter):
遍历序列,选出满足一定标准的元素的处理模式。
对象(object):
变量可以指向的东西。一个对象有数据类型和值。
相等(equivalent):
有相同的值。
相同(identical):
是同一个对象(隐含着相等)。
引用(reference):
一个变量和它的值之间的关联。
别名使用:
两个或者两个以上变量指向同一个对象的情况。
分隔符(delimiter):
一个用于指示字符串分割位置的字符或者字符串。
练习题
你可以从 http://thinkpython2.com/code/list_exercises.py 下载这些练习题的答案。
习题10-1
编写一个叫做 nested_sum
的函数,接受一个由一些整数列表构成的列表作为参数,并将所有嵌套列表中的元素相加。 例如:
>>> t = [[1, 2], [3], [4, 5, 6]] >>> nested_sum(t) 21
习题10-2
编写一个叫做 cumsum
的函数,接受一个由数值组成的列表,并返回累加和; 即一个新列表,其中第i个元素是原列表中前i+1个元素的和。 例如:
>>> t = [1, 2, 3] >>> cumsum(t) [1, 3, 6]
习题10-3
编写一个叫做 middle
的函数,接受一个列表作为参数,并返回一个除了第一个和最后一个元素的列表。 例如:
>>> t = [1, 2, 3, 4] >>> middle(t) [2, 3]
习题10-4
编写一个叫做 chop
的函数,接受一个列表作为参数,移除第一个和最后一个元素,并返回None。 例如:
>>> t = [1, 2, 3, 4] >>> chop(t) >>> t [2, 3]
习题10-5
编写一个叫做``is_sorted``的函数,接受一个列表作为参数, 如果列表是递增排列的则返回 True
,否则返回False。 例如:
>>> is_sorted([1, 2, 2]) True >>> is_sorted(['b', 'a']) False
习题10-6
如果可以通过重排一个单词中字母的顺序,得到另外一个单词,那么称这两个单词是变位词。 编写一个叫做 is_anagram
的函数,接受两个字符串作为参数, 如果它们是变位词则返回 True
。
习题10-7
编写一个叫做 has_duplicates
的函数,接受一个列表作为参数, 如果一个元素在列表中出现了不止一次,则返回 True
。 这个函数不能改变原列表。
习题10-8
这个习题与所谓的生日悖论有关。 你可以在 http://en.wikipedia.org/wiki/Birthday_paradox 中了解更多相关的内容。
如果你的班级上有23个学生, 2个学生生日相同的概率是多少? 你可以通过随机产生23个生日,并检查匹配来估算概率。 提示:你可以使用 random
模块中的 randint
函 数来生成随机生日。
你可以从 http://thinkpython2.com/code/birthday.py 下载我的答案。
习题10-9
编写一个函数,读取文件 words.txt
,建立一个列表,其中每个单词为一个元素。 编写两个版本,一个使用 append
方法,另一个使用 t = t + [x]
。 那个版本运行得慢?为什么?
答案: http://thinkpython2.com/code/wordlist.py 。
习题10-10
你可以使用 in 运算符检查一个单词是否在单词表中,但这很慢,因为它是按顺序查找单词。
由于单词是按照字母顺序排序的,我们可以使用两分法(也称二进制搜索)来加快速度, 类似你在字典中查找单词的方法。 你从中间开始,如果你要找的单词在中间的单词之前,你查找前半部分,否则你查找后半部分。
不管怎样,你都会将搜索范围减小一半。 如果单词表有 113,809 个单词,你只需要 17步就可以找到这个单词,或着得出单词不存在的结论。
编写一个叫做 in_bisect
的函数,接受一个已排序的列表和一个目标值作为参数, 返回该值在列表中的位置,如果不存在则返回 None
。
或者你可以阅读 bisect
模块的文档并使用它!
答案: http://thinkpython2.com/code/inlist.py.
习题10-11
两个单词中如果一个是另一个的反转,则二者被称为是“反转词对”。 编写一个函数,找出单词表中所有的反转词对。
解答: http://thinkpython2.com/code/reverse_pair.py 。
习题10-12
如果交替的从两个单词中取出字符将组成一个新的单词,这两个单词被称为是“连锁词”。 例如,“ shoe”和“ cold”连锁后成为“schooled”。
答案: http://thinkpython2.com/code/interlock.py 。 致谢:这个练习的灵感来自网站 http://puzzlers.org 的一个示例。
- 编写一个程序,找出单词表中所有的连锁词。提示:不要枚举所有的单词对。
- 你能够找到三重连锁的单词吗?即每个字母依次从3个单词得到。
贡献者
- 翻译:@obserthinker
- 校对:@bingjin
- 参考:@carfly