Think Python/列表
类似于字符串,列表也是一个值的序列。在字符串中,值是字符;在列表中,它们可以是任何类型。列表中的值被称为元素,有时也称为项目。
有几种方法可以创建新的列表;最简单的方法是用方括号 ([
和 ]
) 将元素括起来
[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']
第一个示例是包含四个整数的列表。第二个是包含三个字符串的列表。列表的元素不必是相同的类型。以下列表包含一个字符串、一个浮点数、一个整数以及(当然)另一个列表
['spam', 2.0, 5, [10, 20]]
另一个列表内的列表称为嵌套。
不包含任何元素的列表称为空列表;您可以使用空括号 []
创建一个。
正如您可能预期的那样,您可以将列表值赋给变量
>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> numbers = [17, 123]
>>> empty = []
>>> print cheeses, numbers, empty
['Cheddar', 'Edam', 'Gouda'] [17, 123] []
访问列表元素的语法与访问字符串的字符相同——方括号运算符。方括号内的表达式指定索引。请记住,索引从 0 开始
>>> print cheeses[0]
Cheddar
与字符串不同,列表是可变的。当方括号运算符出现在赋值的左侧时,它标识将被赋值的列表元素。
>>> numbers = [17, 123]
>>> numbers[1] = 5
>>> print numbers
[17, 5]
numbers
的第一个元素(以前是 123)现在是 5。
您可以将列表视为索引和元素之间的关系。这种关系称为映射;每个索引都“映射到”一个元素。以下是一个显示 cheeses
、numbers
和 empty
的状态图
列表由带有“列表”字样的方框表示,方框外部是列表的元素,方框内部是列表的元素。cheeses
指向一个包含三个元素的列表,它们的索引分别是 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 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]
第一个示例将 [0]
重复四次。第二个示例将列表 [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']
列表方法都是空方法;它们修改列表并返回 None
。如果您不小心写了 t = t.sort()
,您会对结果感到失望。
要将列表中的所有数字加起来,您可以使用以下循环
def add_all(t):
total = 0
for x in t:
total += x
return total
total
初始化为 0。每次循环时,x
获取列表中的一个元素。+=
运算符提供了一种更新变量的简短方法
total += x
等效于
total = total + x
随着循环执行,total
累积元素的总和;以这种方式使用的变量有时称为累加器。
将列表中的元素加起来是一个非常常见的操作,Python 提供了它作为内置函数 sum
>>> t = [1, 2, 3]
>>> sum(t)
6
这种将一系列元素组合成单个值的运算有时称为归约。
有时您想遍历一个列表,同时构建另一个列表。例如,以下函数接受一个字符串列表,并返回一个包含大写字符串的新列表
def capitalize_all(t):
res = []
for s in t:
res.append(s.capitalize())
return res
res
初始化为一个空列表;每次循环时,我们附加下一个元素。因此,res
是另一种累加器。
类似于 capitalize_all
的运算有时称为映射,因为它将一个函数(在本例中是方法 capitalize
)“映射”到序列中的每个元素上。
另一个常见的操作是从列表中选择一些元素并返回一个子列表。例如,以下函数接受一个字符串列表,并返回一个只包含大写字符串的列表
def only_upper(t):
res = []
for s in t:
if s.isupper():
res.append(s)
return res
isupper
是一个字符串方法,如果字符串仅包含大写字母,则返回 True
。
类似于 only_upper
的运算称为过滤,因为它选择了一些元素并过滤掉其他元素。
大多数常见的列表操作可以表示为映射、过滤和归约的组合。由于这些操作非常常见,Python 提供了语言特性来支持它们,包括内置函数 map
和一个称为“列表推导”的运算符。
编写一个函数,该函数接受一个数字列表,并返回累积和;也就是说,一个新列表,其中第 'i' 个元素是原始列表中前 'i+1' 个元素的总和。例如,'[1, 2, 3]
' 的累积和是 '[1, 3, 6]
'。
有几种方法可以从列表中删除元素。如果您知道要删除元素的索引,可以使用 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']
与往常一样,切片选择所有元素,直到但不包括第二个索引。
列表和字符串
[edit | edit source]字符串是字符序列,列表是值的序列,但字符列表与字符串不同。要将字符串转换为字符列表,可以使用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']
一个可选参数称为 **分隔符**,它指定哪些字符用作单词边界。以下示例使用连字符作为分隔符
>>> 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
在单词之间放置一个空格。要将字符串连接起来而不留空格,可以使用空字符串''
作为分隔符。
对象和值
[edit | edit source]如果执行这些赋值语句
a = 'banana'
b = 'banana'
我们知道CODE>a
CODE> 和 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
所以状态图看起来像这样
在这种情况下,我们会说这两个列表是 **等效** 的,因为它们具有相同的元素,但不是 **相同** 的,因为它们不是同一个对象。如果两个对象相同,那么它们也是等效的,但如果它们是等效的,则它们不一定相同。
到目前为止,我们一直在交替使用“对象”和“值”,但更准确地说,对象具有值。如果您执行a = [1,2,3]
,a
引用一个列表对象,其值为特定元素序列。如果另一个列表具有相同的元素,我们会说它具有相同的值。
别名
[edit | edit source]如果a
引用一个对象,并且您分配b = a
,那么这两个变量都引用同一个对象
>>> a = [1, 2, 3]
>>> b = a
>>> b is a
True
状态图看起来像这样
变量与对象之间的关联称为 **引用**。在本例中,存在对同一个对象的两个引用。
具有多个引用的对象具有多个名称,因此我们说该对象是 **别名** 的。
如果别名对象是可变的,则使用一个别名进行的更改会影响另一个别名
>>> b[0] = 17
>>> print a
[17, 2, 3]
虽然这种行为可能很有用,但它容易出错。一般来说,在处理可变对象时,最好避免使用别名。
对于像字符串这样的不可变对象,别名并不是什么大问题。在本例中
a = 'banana'
b = 'banana'
a
和 b
是否引用同一个字符串几乎没有区别。
列表参数
[edit | edit source]将列表传递给函数时,函数会获得对该列表的引用。如果函数修改列表参数,则调用者会看到更改。例如,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]
>>> t2 is t3
False
这种区别在编写旨在修改列表的函数时很重要。例如,此函数不会 删除列表的头部
def bad_delete_head(t):
t = t[1:] # WRONG!
切片运算符创建一个新列表,赋值使t
引用它,但所有这些都不会影响作为参数传递的列表。
另一种选择是编写一个创建并返回新列表的函数。例如,tail
返回除第一个元素之外的所有元素
def tail(t):
return t[1:]
此函数不会修改原始列表。以下是它的使用方法
>>> letters = ['a', 'b', 'c']
>>> rest = tail(letters)
>>> print rest
['b', 'c']
练习 2
[edit | edit source]编写一个名为 'chop
' 的函数,它接受一个列表并对其进行修改,删除第一个和最后一个元素,并返回 'None
'。
然后编写一个名为 'middle
' 的函数,它接受一个列表并返回一个包含除第一个和最后一个元素之外所有元素的新列表。
调试
[edit | edit source]不小心使用列表(和其他可变对象)会导致长时间的调试。以下是一些常见的陷阱以及避免它们的方法
- 不要忘记大多数列表方法会修改参数并
返回None
。这与字符串方法相反,字符串方法会返回一个新字符串并保留原始字符串。如果您习惯于编写这样的字符串代码
word = word.strip()
您可能会忍不住编写这样的列表代码
t = t.sort() # WRONG!
因为sort
返回None
,所以您对t
执行的下一个操作很可能会失败。
在使用列表方法和运算符之前,应仔细阅读文档,然后在交互模式下对其进行测试。列表与其他序列(如字符串)共享的方法和运算符在docs.python.org/lib/typesseq.html
中有记录。仅适用于可变序列的方法和运算符在docs.python.org/lib/typesseq-mutable.html
中有记录。
- 选择一种习惯用法并坚持使用它。
列表问题的一部分是,有太多种方法可以做事情。例如,要从列表中删除一个元素,可以使用pop
、remove
、del
,甚至可以使用切片赋值。
要添加一个元素,可以使用append
方法或+
运算符。但不要忘记这些是正确的
t.append(x)
t = t + [x]
而这些是错误的
t.append([x]) # WRONG!
t = t.append(x) # WRONG!
t + [x] # WRONG!
t = t + x # WRONG!
尝试在交互模式下运行每个示例,以确保您了解它们的作用。请注意,只有最后一个会导致运行时错误;其他三个是合法的,但它们执行了错误的操作。
- 创建副本以避免使用别名。
如果您想使用sort
之类的会修改参数的方法,但您也需要保留原始列表,则可以创建副本。
orig = t[:]
t.sort()
在本例中,您也可以使用内置函数sorted
,它返回一个新的排序列表并保留原始列表。但在这种情况下,您应该避免使用sorted
作为变量名!
术语表
[edit | edit source]- 列表
- 值的序列。
- 元素
- 列表(或其他序列)中的一个值,也称为项。
- 索引
- 表示列表中一个元素的整数值。
- 嵌套列表
- 作为另一个列表的元素的列表。
- 列表遍历
- 按顺序访问列表中的每个元素。
- 映射
- 一种关系,其中一个集合中的每个元素都对应于另一个集合中的元素。例如,列表是索引到元素的映射。
- 累加器
- 在循环中用于累加或累积结果的变量。
- 减少
- 一种处理模式,它遍历序列并将元素累积到单个结果中。
- 映射
- 一种处理模式,它遍历序列并对每个元素执行操作。
- 过滤
- 一种处理模式,它遍历列表并选择满足某些条件的元素。
- 对象
- 变量可以引用的东西。对象具有类型和值。
- 等效
- 具有相同的值。
- 相同
- 是同一个对象(这意味着等效)。
- 引用
- 变量与其值之间的关联。
- 别名
- 两个变量引用同一个对象的情况。
- 分隔符
- 用于指示应在何处拆分字符串的字符或字符串。
练习
[edit | edit source]编写一个名为 is_sorted
的函数,该函数以列表作为参数,如果列表按升序排序,则返回 'True
',否则返回 'False
'。您可以假设(作为先决条件)列表的元素可以使用比较运算符 '<
'、'>
' 等进行比较。
例如,is_sorted([1,2,2])
应该返回 'True
',而 is_sorted(['b','a'])
应该返回 'False
'。
如果您可以重新排列一个单词的字母以拼出另一个单词,那么这两个单词就是回文。编写一个名为 is_anagram
的函数,该函数接受两个字符串并返回 'True
',如果它们是回文。
(所谓的)生日悖论
- 编写一个名为
has_duplicates
的函数,该函数接受一个列表,如果列表中存在任何元素出现超过一次,则返回 'True
'。它不应该修改原始列表。
- 如果你的班级有 23 名学生,你们中有两人生日相同的概率是多少?您可以通过生成 23 个生日的随机样本并检查匹配项来估计此概率。提示:您可以使用 '
random
' 模块中的 'randint
' 函数生成随机生日。
您可以在 'wikipedia.org/wiki/Birthday_paradox
' 上阅读有关此问题的信息,您也可以在 'thinkpython.com/code/birthday.py
' 中查看我的解决方案。
编写一个名为 remove_duplicates
的函数,该函数接受一个列表并返回一个新列表,其中只包含原始列表中的唯一元素。提示:它们不必按相同的顺序排列。
编写一个函数,该函数读取 'words.txt
' 文件并构建一个列表,每个单词一个元素。编写此函数的两个版本,一个使用 'append
' 方法,另一个使用习惯用法 't = t + [x]
'。哪个运行时间更长?为什么?
您可以在 'thinkpython.com/code/wordlist.py
' 中查看我的解决方案。
要检查单词是否在单词列表中,您可以使用 'in
' 运算符,但这会很慢,因为它会按顺序搜索单词。
由于单词按字母顺序排列,我们可以通过二分搜索来加快速度,这与您在字典中查找单词时所做的类似。您从中间开始,检查您要查找的单词是否位于列表中间的单词之前。如果是,则以相同的方式搜索列表的前半部分。否则,您将搜索后半部分。
无论哪种方式,您都会将剩余的搜索空间减半。如果单词列表有 113,809 个单词,则大约需要 17 步才能找到该单词或得出它不存在的结论。
编写一个名为 'bisect
' 的函数,该函数接受一个排序的列表和一个目标值,并返回列表中该值的索引(如果存在),如果不存在,则返回 'None
'。
或者您可以阅读 'bisect
' 模块的文档并使用它!
如果一个单词是另一个单词的反转,则这两个单词被称为“反转对”。编写一个程序,查找单词列表中的所有反转对。
如果从每个单词中交替取字母形成一个新词1,则两个单词“互锁”。例如,“shoe” 和 “cold” 互锁形成 “schooled”。
- 编写一个程序,查找所有互锁的单词对。提示:不要枚举所有对!
- 你能找到任何三个方向互锁的单词吗?也就是说,从第一个、第二个或第三个字母开始,每隔三个字母形成一个单词?
- 1
- 本练习的灵感来自
puzzlers.org
上的一个示例。