跳转至

第十章 列表

本章讲述的是 Python 里面最有用的一种内置类型:列表。你还能在本章中学到更多关于类的内容,你还会看到如果同一个对象有多个名字会有什么情况发生。

10.1 列表也是序列

和字符串差不多,列表是一系列的数值的序列。在字符串里面,这些值是字符;在列表里面,这些值可以是任意类型的。一个列表中的值一般叫做列表的元素,有时候也叫列表项。

创建一个新的列表有好几种方法;最简单的方法就是把列表的元素用方括号包含起来:

[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']

第一个例子建立了一个由四个整形变量组成的列表。第二个是一个由三个字符串组成的列表。

['spam', 2.0, 5, [10, 20]]

列表内部可以包含一个列表作为元素,这种包含列表的列表也叫做网状列表。

不含有任何元素的列表叫做空列表;可以就用空的方括号来建立一个。

你估计也会预料到,列表的值可以赋给变量:

>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> numbers = [42, 123]
>>> empty = []
>>> print(cheeses, numbers, empty)
['Cheddar', 'Edam', 'Gouda']
[42, 123]
[]

10.2 列表元素可修改

读取列表元素的语法就如同读取字符串中的字符一样-用方括号运算符就可以了。方括号内的数字用来确定索引位置。一定要记住,Python 是从零开始计数的:

>>> cheeses[0]
'Cheddar'

和字符串不同的是,列表是可以修改的。方括号运算符放到一个赋值语句的等号左侧的时候,就会把对应位置的列表元素重新赋值。

>>> numbers = [42, 123]
>>> numbers[1] = 5
>>> numbers
[42, 5]

列表 numbers 的第『1』个元素之前是 123,现在被改为 5 了。

图 10.1 展示了 cheeses、numbers 和空列表的状态图:


Figure 10.1: State diagram Figure 10.1: State diagram.


这三个列表都用标记了『list』的三个小盒子表示,盒子外面的是列表的名字,盒子内的就是列表元素了。cheese 是一个有 0,1,2 三个元素的列表。numbers 包含两个元素;图示表明第二个元素的值从 123 被重新赋值为 5。empty 是一个不包含元素的空列表。

列表的索引和字符串的索引的格式是一样的:

• 任意的一个整型表达式,都可以用来作为索引编号。

• 如果你试图读取或者写入一个不存在的列表元素,你就会得到一个索引错误 IndexError。

• 如果一个索引是负值,意味着是从列表末尾向前倒序计数查找相对应的位置。

在列表中也可以使用 in 运算符。

>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> 'Edam' in cheeses
True
>>> 'Brie' in cheeses
False

10.3 遍历一个列表

遍历一个列表中所有元素的最常用的办法就是 for 循环了。这种 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]]

10.4 列表运算符

加号+运算符可以把列表拼接在一起:

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

第一个例子中,[0]这个列表被重复四次。第二个例子把列表[1,2,3]重复了三次。

10.5 列表切片

切片操作符也可以用到列表上:

>>> 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
>>> t
['a', 'x', 'y', 'd', 'e', 'f']

10.6 列表的方法

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 把列表中的元素从低到高(译者注:下面的例子中是按照 ASCII 码的大小从小到大)排列:

>>> t = ['d', 'c', 'e', 'b', 'a']
>>> t.sort()
>>> t
['a', 'b', 'c', 'd', 'e']

大多数列表的方法都是无返回值的;这些方法都对列表进行修改,返回的是空。如果你不小心写出了一个 t=t.sort(),得到的结果恐怕让你很失望。

10.7 Map, filter, reduce 列表中最重要的三种运算

要得到列表中所有值的综合,你可以用下面这样的一个循环来实现:

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

把一系列列表元素组合成一个单值的运算,也叫做 reduce(这个单词是缩减的意思)。

有时候建立一个列表需要遍历另一个列表。比如下面的这个函数就接收一个字符串列表,将所有字符串变为大写字母组成的,然后返回一个这些大写字符串组成的新列表:

def capitalize_all(t):
    res = []
    for s in t:
        res.append(s.capitalize())
    return res

res 的初始值为一个空列表;每次循环的时候,我们都把下一个元素用 append 方法拼接上去。所以 res 也算是另外一种累加器了。

像上面这个 capitalize_all 的运算也叫做一个 map(单词意思为地图),因为这种预算将某一函数(该例子中是 capitalize 这个方法)应用到一个序列中的每个元素上。

另外一种常见运算是从列表中选取一些元素,然后返回一个次级列表。比如,下面的函数接收一个字符串列表,然后返回一个其中只包含大写字母的字符串所组成的列表:

def only_upper(t):
    res = []
    for s in t:
        if s.isupper():
            res.append(s)
    return res

isupper 是一个字符串方法,如果字符串中只包含大写字母就会返回真。

only_upper 这样的运算也叫 filter(过滤器的意思),因为这种运算筛选出特定的元素,过滤掉其他的。

常用的列表运算都可以表示成 map、filter 以及 reduce 的组合。

10.8 删除元素

从一个列表中删除元素有几种方法。如果你知道你要删除元素的索引,你就可以用 pop 这个方法来实现:

>>> t = ['a', 'b', 'c']
>>> x = t.pop(1)
>>> t
['a', 'c']
>>> x
'b'

pop 修改列表,然后返回删除的元素。如果你不指定一个索引位置,pop 就会删除和返回最后一个元素。

如果你不需要删掉的值了,你可以用 del 运算符来实现:

>>> t = ['a', 'b', 'c']
>>> del t[1]
>>> t
['a', 'c']

如果你知道你要删除的元素值,但不知道索引位置,你可以使用 remove 这个方法:

>>> t = ['a', 'b', 'c']
>>> t.remove('b')
>>> t
['a', 'c']

remove 的返回值是空。

要删除更多元素,可以使用 del 和切片索引:

>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> del t[1:5]
>>> t
['a', 'f']

和之前我们看到的一样,切片是含头不含尾,上面这个例子中从第『1』到第『5』个都被切片所选中,但包含开头的第『1』而不包含末尾的第『5』个元素。

10.9 列表和字符串

字符串是一系列字符的序列,而李白是一系列值的序列,但一个由字符组成的列表是不同于字符串的。要把一个字符串转换成字符列表,你可以用 list 这个函数:

>>> s = 'spam'
>>> t = list(s)
>>> t
['s', 'p', 'a', 'm']

list 是一个内置函数的名字了,所以你应该避免用它来作为变量名。我还建议应该尽量少用 l,因为有的字体下,l 和 1 看着很难区分。所以我都用了 t。

list 这个函数将一个字符串分开成一个个字母。如果你想把字符串切分成一个个单词,你可以用 split 这个方法:

>>> s = 'pining for the fjords'
>>> t = s.split()
>>> t
['pining', 'for', 'the', 'fjords']

可选的参数是定界符,是用来确定单词边界的。下面这个例子中就是把连接号『-』作为定界符:

>>> s = 'spam-spam-spam'
>>> delimiter = '-'
>>> t = s.split(delimiter)
>>> t
['spam', 'spam', 'spam']

join 是与 split 功能相反的一个方法。它接收一个字符串列表,然后把所有元素拼接到一起。join 是一个字符串方法,所以必须把 join 放到定界符后面来调用,并且传递一个列表作为参数:

>>> t = ['pining', 'for', 'the', 'fjords']
>>> delimiter = ' '
>>> s = delimiter.join(t)
>>> s
'pining for the fjords'

上面这个例子中,定界符是一个空格字符,所以 join 就在单词只见放一个空格。要想把字符聚集到一切而不要空格,你就可以用空字符串""作为一个定界符了。

10.10 对象和值

如果我们运行下面这种赋值语句:

a = 'banana'
b = 'banana'

我们知道了 a 和 b 都是字符串,但我们不之道他们到底是不是同一个字符串。这就有可能有两种状态,如图 10.2 所示。


Figure 10.2: State diagram Figure 10.2: State diagram.


在第一种情况中,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

所以这时候的状态图就应该如图 10.3 所示的样子了。


Figure 10.3: State diagram.


在这个情况下,我们可以说两个列表是相等的,因为它们有相同的元素,但它们不是同一个列表,因为他们并不是同一个对象。如果两个对象是同一个对象,那它们必然是相等的,但如果它们相等,却未必是同一个对象。

(译者注:同一 是 相等 的充分条件,相等 是 同一 的必要条件,仅此而已。)

目前位置,我们一直把『对象』和『值』来随意交换使用,但实际上更确切的说法是一个对象拥有一个值。如果你计算[1,2,3],你得到一个列表对象,整个列表对象的整个值是一个整数序列。如果另外一个列表有相同的元素,我们称之含有相同的值,但并不是相同的对象。

10.11 别名

如果 a 是一个对象了,然后你赋值 b=a,那么这两个变量都指向同一个对象:

>>> a = [1, 2, 3]
>>> b = a
>>> b is a
True

此时的状态图如图 10.4 所示。


Figure 10.4: State diagram Figure 10.4: State diagram.


一个变量和一个对象的关系叫做引用。在上面这个例子中,a 和 b 是对同一对象的两个引用。

这样一个对象有不止一个引用,就也有了不止一个名字,所以就说这个对象有别名了。

如果一个别名对象是可修改的,那么对一个别名做出的修改就会影响到其他引用:

>>> b[0] = 42
>>> a
[42, 2, 3]

这一性质是很有用处的,但很容易让初学者犯错。所以一般来说,处理可变对象的时候,还是尽量避免别名使用,这样更安全些。

对于不可变对象比如字符串来说,别名使用就不是太大问题了。如下所示:

a = 'banana'
b = 'banana'

a 和 b 是否指向同一个字符就基本无所谓了,几乎不会有任何影响。

10.12 列表参数

当你传递一个列表给一个函数的时候,函数得到的是对该列表的一个引用。如果函数修改了列表,调用者会看到变化的。比如下面这个 delete_head 函数就从列表中删除第一个元素:

def delete_head(t):
    del t[0]

其用法如下:

>>> letters = ['a', 'b', 'c']
>>> delete_head(letters)
>>> letters ['b', 'c']

形式参数 t 和变量 letters 都是同一对象的别名。栈图如图 10.5 所示。


Figure 10.5: Stack diagram Figure 10.5: Stack diagram.


因为这个列表被两个框架所公用,我就把它画在了它们之间。

一定要区分修改列表的运算和产生新列表的运算,这特别重要。比如 append 方法修改一个列表,但加号+运算符是产生一个新的列表:

>>> t1 = [1, 2]
>>> t2 = t1.append(3)
>>> t1
[1, 2, 3]
>>> t2
None

append 修改了列表,返回的是空。

>>> t3 = t1 + [4]
>>> t1
[1, 2, 3]
>>> t3
[1, 2, 3, 4]
>>> t1
[1, 2, 3]

加号+运算符创建了新的列表,并不修改源列表。

这以区别相当重要,尤其是当你写一些要修改列表的函数的时候。比如下面这个函数并没有能够删除列表的第一个元素:

def bad_delete_head(t):
    t = t[1:]              # WRONG!

切片运算符创建了新的列表,然后赋值语句让 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']

10.13 调试

对列表或者其他可变对象,用的不小心的货,就会带来很多麻烦,需要好长时间来调试。下面是一些常见的陷阱,以及避免的方法:

1. 大多数列表方法都修改参数,返回空值。这正好与字符串方法相反,字符串的方法都是返回一个新字符串,保持源字符串不变。

如果你习惯些下面这种字符串代码了:

word = word.strip()

你估计就会写出下面这种列表代码:

t = t.sort()           # WRONG!

因为 sort 返回的是空值,所以对 t 的后续运算都将会失败。

在使用列表的方法和运算符之前,你应该好好读一下文档,然后在交互模式里面对它们进行一下测试。

2. 选一种方法并坚持使用。

列表使用的问题中很大一部分都是因为有太多方法来实现目的导致的。例如要从一个列表中删除一个元素,可以用 pop,remove,del 甚至简单的切片操作。

要加一个元素,可以用 append 方法或者加号+运算符。假设 t 是一个列表,而 x 是一个列表元素,下面的代码都是正确的:

t.append(x)
t = t + [x]
t += [x]

下面这就都是错的了:

t.append([x])          # WRONG!
t = t.append(x)        # WRONG!
t + [x]                # WRONG!
t = t + x              # WRONG!

在交互模式下试试上面这些例子,确保你要理解它们的作用。要注意只有最后一个会引起运行错误,其他的三个都是合法的,但产生错误的效果。

3. 尽量做备份,避免用别名。

如果你要用 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]

10.14 Glossary 术语列表

list: A sequence of values.

列表:一系列值的序列。

element: One of the values in a list (or other sequence), also called items.

元素:一个列表或者其他序列中的值,也叫项。

nested list: A list that is an element of another list.

网状列表:一个作为其他列表元素的列表。

accumulator: A variable used in a loop to add up or accumulate a result.

累加器:一种用来在循环中累加或者拼接结果的变量。

augmented assignment: A statement that updates the value of a variable using an operator like+=.

增强赋值语句:使用+=这种自增运算符来更新变量值的语句。

reduce: A processing pattern that traverses a sequence and accumulates the elements into a single result.

reduce:一种处理模式,遍历一个序列,把元素积累起来结合成一个单独的结果。

map: A processing pattern that traverses a sequence and performs an operation on each element.

map:一种处理模式,遍历一个序列,对每一个元素都进行某种运算。

filter: A processing pattern that traverses a list and selects the elements that satisfy some criterion.

filter:一种处理模式,遍历一个列表,选取其中满足特定规则的一些元素。

object: Something a variable can refer to. An object has a type and a value.

对象:变量所指向的就是对象。一个对象有特定的某个类型,以及一个值。

equivalent: Having the same value.

相等:有相等的值。

identical: Being the same object (which implies equivalence).

相同:是同一个对象(意味着必然就是相等了)。

reference: The association between a variable and its value.

引用:变量 a 与其值的关系。

aliasing: A circumstance where two or more variables refer to the same object.

别名:同一个对象有两个或者更多变量所指向的情况。

delimiter: A character or string used to indicate where a string should be split.

定界符:一个字符或者字符串,用来确定字符分割时候的分界。

10.15 练习

你可以从 这里 下载下面这些练习的样例代码。

练习 1

写一个函数,名为 nested_sum,接收一系列的整数列表,然后把所有分支列表中的元素加起来。如下所示:

>>> t = [[1, 2], [3], [4, 5, 6]]
>>> nested_sum(t)
21

练习 2

写一个函数,明切 cumsum,接收一个数字列表,然后返回累加的总和;也就是新列表的第 i 个元素就是源列表中前 i+1 个元素的累加。如下所示:

>>> t = [1, 2, 3]
>>> cumsum(t)
[1, 3, 6]

练习 3

写一个函数,名为 middle,接收一个列表,返回一个新列表,新列表要求对源列表掐头去尾只要中间部分。如下所示:

>>> t = [1, 2, 3, 4]
>>> middle(t)
[2, 3]

练习 4

写一个函数,名为 chop,接收一个列表,修改这个列表,掐头去尾,返回空值。如下所示:

>>> t = [1, 2, 3, 4]
>>> chop(t)
>>> t
[2, 3]

练习 5

写一个函数,名为 is_sorted,接收一个列表作为参数,如果列表按照字母顺序升序排列,就返回真,否则返回假。如下所示:

>>> is_sorted([1, 2, 2])
True
>>> is_sorted(['b', 'a'])
False

练习 6

两个单词如果可以通过顺序修改来互相转换就互为变位词。写一个函数,名为 is_anagram,接收两个字符串,如果互为变位词就返回真。

练习 7

写一个函数,名为 has_duplicates,接收一个列表,如果里面有重复出现的元素,就返回真。这个函数不能修改源列表。

练习 8

这个练习也可以叫做生日悖论,你可以点击这里来读一下更多背景知识。

假如你班上有 23 个学生,这当中两个人同一天出生的概率是多大?你可以评估一下 23 个随机生日中有相同生日的概率。提示一下:你可以用 randint 函数来生成随机的生日,这个函数包含在 random 模块中。

你可以从 这里下载我的样例代码。

练习 9

写一个函数,读取文件 words.txt,然后建立一个列表,这个列表中每个元素就是这个文件中的每个单词。写两个版本的这样的函数,一个使用 append 方法,另外一个用自增运算的模式:t= t + [x]。看看哪个运行时间更长?为什么会这样?

样例代码

练习 10

要检查一个单词是不是在上面这个词汇列表里,你可以使用 in 运算符,但可能会很慢,因为这个 in 运算符要从头到尾来搜索整个词汇表。

我们知道这些单词是按照字母表顺序组织的,所以我们可以加速一下,用一种对折搜索(也叫做二元搜索),这个过程就和你在现实中用字典来查单词差不多。你在中间部分开始,看看这个要搜索的词汇是不是在中间位置的前面。如果在前面,就又对前半部分取中间,继续这样来找。当然了,不在前半部分,就去后半部分找了,思路是这样的。

不论怎样,每次都会把搜索范围缩减到一半。如果词表包含了 113809 个单词,最多就是 17 步就能找到单词,或者能确定单词不在词汇表中。

那么问题来了,写一个函数,名为 in_bisect,接收一个整理过的按照字母顺序排列的列表,以及一个目标值,在列表中查找这个值,找到了就返回索引位置,找不到就返回空。

样例代码.

1 练习 11

两个词如果互为逆序,就称它们是『翻转配对』。写一个函数来找一下在这个词汇表中所有这样的词对。样例代码

1 练习 12

两个单词,依次拼接各自的字母,如果能组成一个新单词,就称之为『互锁』。比如,shoe 和 cold就可以镶嵌在一起组成组成 schooled。(译者注:shoe+cold= schooled样例代码. 说明:这个练习受到了这里一处例子的启发。

  1. 写一个程序,找到所有这样的互锁单词对。提示:不要枚举所有的单词对!

  2. 你能找到那种三路互锁的单词么;就是那种三三排列三个单词的字母能组成一个单词的三个词?



回到顶部