数据结构
这一章对你已经学习过的内容进行深入探讨,并且会加入一些新的内容。
5.1 More on Lists
list
数据类型还有一些方法。下列是 list
对象的所有方法:
list.append(x)
在列表末尾添加一个项。等价于 a[len(a):] = [x]
。
list.extend(iterable)
将 iterable
中所有的项都加到列表中。等价于 a[len(a):] = iterable
。
list.insert(i, x)
在指定位置插入一个项。第一个参数是要插入的位置的索引,因此 a.insert(0, x)
在列表头部进行插入,a.insert(len(a), x)
等价于 a.append(x)
。
list.remove(x)
移除列表中第一个值为 x 的项。如果不存在值为 x 的项,会抛出 ValueError
异常。
list.pop([i])
移除并返回指定位置的项。如果没有指定索引,a.pop
移除并返回列表的最后一个项。(在方法签名中 i
周围的方括号表示这个参数是可选的,不是让你把方括号也一起敲入。在Python Library Reference 中你会经常见到这些方括号。)
list.clear()
移除列表中所有的项。等价于 del a[:]
。
list.index(x[, start[, end]])
返回第一个值为 x
的项的索引(以零为基准)。如果列表中不存在这样的 x
,抛出 ValueError
异常。
可选参数 start
和 end
被解释为切片表示法,并且用于将搜索范围缩小为一个列表的特定子序列。index()
方法返回的索引是从整个序列的开头开始计数的,而不是以 start
为头开始计数的。
list.count(x)
返回 x 在列表中出现的次数。
list.sort(key=None, reverse=False)
对列表中的元素进行排序(函数参数可以用于定制排序规则,详细见 sorted())
list.reverse()
反转列表。
list.copy()
返回列表的一份拷贝。等价于 a[:]
。
列表方法使用示例:
>>> fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']
>>> fruits.count('apple')
2
>>> fruits.count('tangerine')
0
>>> fruits.index('banana')
3
>>> fruits.index('banana', 4) # Find next banana starting a position 4
6
>>> fruits.reverse()
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange']
>>> fruits.append('grape')
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange', 'grape']
>>> fruits.sort()
>>> fruits
['apple', 'apple', 'banana', 'banana', 'grape', 'kiwi', 'orange', 'pear']
>>> fruits.pop()
'pear'
你可能注意到了像 insert
、remove
或者 sort
这样的方法只修改列表,却没有显示返回值 —— 那是因为它们返回了默认的 None
。这是 Python 中所有可变数据结构的一个设计原则。
可能你还注意到了不是所有数据都可以被排序或者比较的。比如,[None, 'hello', 10]
不能进行排序,因为 integer 不能和 string 进行比较,None 不能和其他类型进行比较。还有,有些类型没有被定义顺序关系。比如,3+4j < 5+7j
不属于合法的比较。
5.1.1. 把列表当作栈来使用
列表的方法使得列表作为栈(stack)来使用变得非常简单,最后添加的元素就是最先取出的元素(后进先出)。想要在栈顶添加元素,就用 append()
。想要从栈顶取出一个元素,就用没有指定索引的 pop()
。例如:
>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
>>> stack.pop()
6
>>> stack.pop()
5
>>> stack
[3, 4]
5.1.2. 将列表作为队列来使用
还可以将列表作为队列来使用,最先添加的元素会被最先取出(先进先出)。但是,用列表实现的队列并不高效。列表尾部的插入和弹出操作可以很快的进行,但在列表头部进行的插入和弹出操作却很慢(因为所有插入位置后的元素都必须移动一个单位)。
要想实现队列,就使用 collection.deque
,它被设计成可以快速地对两端进行 append
和 pop
操作。例如:
>>> from collections import deque
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry") # Terry arrives
>>> queue.append("Graham") # Graham arrives
>>> queue.popleft() # The first to arrive now leaves
'Eric'
>>> queue.popleft() # The second to arrive now leaves
'John'
>>> queue # Remaining queue in order of arrival
deque(['Michael', 'Terry', 'Graham'])
5.1.3. 列表推导式
列表推导式(list comprehension
)提供了创建列表的简洁方法。一般用于创建新列表,新列表中每个元素都是将一些运算作用于另一个 sequence
或者 iterable
中的元素后产生的结果;或者用于创建这些元素的满足特定条件的子序列。
比如,假设我们想要创建一个平方数的列表:
>>> squares = []
>>> for x in range(10):
... squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
注意变量 x
在这个过程中被创建(或覆写),并且它在循环结束后仍然可以使用。我们可以毫无副作用地创建一个平方数列表:
squares = list(map(lambda x: x**2, range(10)))
或者,等价地:
squares = [x**2 for x in range(10)]
这更加简洁,更具可读性。
一个列表推导式方括号组成,其中包含一个表达式,表达式后面跟着 for
子句,for
子句后面还可以有 0
个或多个 for
或 if
子句。
列表推导的结果是一个新的列表,这个列表根据其后的 for
和 if
子句组成的上下文推导得出。例如,下面的列表推导式将两个列表中不同的元素进行组合:
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
这等同于:
>>> combs = []
>>> for x in [1,2,3]:
... for y in [3,1,4]:
... if x != y:
... combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
注意,在这两个代码片段中,for
和 if
语句的顺序是相同的。
如果表达式是一个 tuple
(如,前面例子中的 (x, y)
),就必须用圆括号括起来:
>>> vec = [-4, -2, 0, 2, 4]
>>> # create a new list with the values doubled
>>> [x*2 for x in vec]
[-8, -4, 0, 4, 8]
>>> # filter the list to exclude negative numbers
>>> [x for x in vec if x >= 0]
[0, 2, 4]
>>> # apply a function to all the elements
>>> [abs(x) for x in vec]
[4, 2, 0, 2, 4]
>>> # call a method on each element
>>> freshfruit = [' banana', ' loganberry ', 'passion fruit ']
>>> [weapon.strip() for weapon in freshfruit]
['banana', 'loganberry', 'passion fruit']
>>> # create a list of 2-tuples like (number, square)
>>> [(x, x**2) for x in range(6)]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
>>> # the tuple must be parenthesized, otherwise an error is raised
>>> [x, x**2 for x in range(6)]
File "<stdin>", line 1, in <module>
[x, x**2 for x in range(6)]
^
SyntaxError: invalid syntax
>>> # flatten a list using a listcomp with two 'for'
>>> vec = [[1,2,3], [4,5,6], [7,8,9]]
>>> [num for elem in vec for num in elem]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
列表推导式可以包含复杂的表达式和嵌套的函数:
>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']
5.1.4. 嵌套的列表推导式
列表推导式中的初始化表达式可以是任何表达式,包括列表推导式。
考虑下面这个 3x4 的矩阵,它被一个列表所实现,这个列表中包含 3 个长度为 4 的列表:
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
下面的列表推导式将对行和列进行转换:
>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
正如我们在上一节看到的,the nested listcomp is evaluated in the context of the for
that follows it#TODO
所以上面的例子等同于:
>>> transposed = []
>>> for i in range(4):
... transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
依次的,上面的代码等同于:
>>> transposed = []
>>> for i in range(4):
... # the following 3 lines implement the nested listcomp
... transposed_row = []
... for row in matrix:
... transposed_row.append(row[i])
... transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
实际应用中,与复杂的流语句相比,你可能对内置函数更感兴趣。zip()
函数将能很好的处理这类问题:
>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]
有关这一行中星号的更多内容,请看Unpacking Argument Lists。
5.2. del 语句 ✔
有一种方式可以使用列表的索引,而不是元素值来删除列表的元素:del
语句。del 语句与有返回值的 pop()
方法不同。del 语句还可用于移除列表的切片或者清空整个列表(之前我们将一个空列表赋值给切片来清空列表):
>>> a = [-1, 1, 66.25, 333, 333, 1234.5]
>>> del a[0]
>>> a
[1, 66.25, 333, 333, 1234.5]
>>> del a[2:4]
>>> a
[1, 66.25, 1234.5]
>>> del a[:]
>>> a
[]
del
还可以用于删除一个变量:
>>> del a
此后引用变量 a
将会产生错误(至少在赋新的值给它前是这样)。后面我们会研究 def
的更多用途。
5.3. Tuples 和 Sequences ✔
我们可以看到 list
和 string
有许多共通的地方,比如索引操作和切片操作。它们是 sequance
类型的两个例子(见Sequence Types — list, tuple, range)。因为 Python 还在不断发展,所以其他的数据类型可能会被加入到 Python 中。还有一种标准的 sequence
数据类型:tuple
。
一个 tuple
由用逗号分隔的一些值组成,如:
>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples 可以嵌套:
... u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples 不可修改:
... t[0] = 88888
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # 但可以包含可修改的对象:
... v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])
如你所见,在输出时,元组总是被圆括号括起来,这样就可以更准确地解释嵌套的元组。元组可能在输入时带有括号,也可能不带括号,尽管通常来说圆括号是必要的(如果元组是一个更大的表达式中的一部分)。不能将一个值赋给元组中的独立元素,但是可以创建带有可修改对象(如列表)的元组。
尽管 tuple
可能看上去和 list
很像,但是它们经常用于不同的场景和不同的目的。元组是不可修改的,通常包含一个序列的异构(heterogeneous)的元素,这些元素通过解包(见本节后面的内容)或索引来访问(在 namedtuples
中,甚至可以通过属性来访问)。list
是可修改的,list
的元素通常是同构的(homogeneous),并通过遍历 list
来访问。
构建包含0个或1个元素的元组是一个特别的问题:Python 语法对此有额外的兼容来适应这个问题。空元组通过一对圆括号来构建;只有一个元素的元组通过跟随着一个逗号的值来构建(仅用括号括起一个值是不够的)。有点丑,但是很高效。如:
>>> empty = ()
>>> singleton = 'hello', # <-- 注意最后的逗号
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)
语句 t = 12345, 54321, 'hello!'
是 tuple packing
的一个例子:12345
,54321
和 'hello!'
被 pack
在了一个元组里。相反的操作同样可行:
>>> x, y, z = t
这被称为 sequence unpacking
,这对右边的任何 sequence
都起作用。sequence unpacking
要求等号左边的变量个数与 sequence
中的元素个数相同。注意,多重赋值(multiple assignment)其实就是 tuple packing
和 sequence unpacking
的组合。
5.4. 集合 ✔
Python 还支持 set
。set
是一种没有重复元素的无序集合。基本用法有 membership testing 和消除重复项。set
对象支持一些数学运算,如并、交、差和对称差。
可以用花括号或 set()
函数来创建 set
。注意,如果要创建一个空的 set
,必须使用set()
,而不是{}
,后者会创建一个空的字典(一种我们下一节会讨论的数据结构)。
这里有些简短的示例:
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket) # show that duplicates have been removed
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket # fast membership testing
True
>>> 'crabgrass' in basket
False
>>> # Demonstrate set operations on unique letters from two words
...
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a # unique letters in a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # letters in a but not in b
{'r', 'd', 'b'}
>>> a | b # letters in a or b or both
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # letters in both a and b
{'a', 'c'}
>>> a ^ b # letters in a or b but not both
{'r', 'd', 'b', 'm', 'z', 'l'}
与列表推导式类似,set
推导式也被 Python 支持:
>>> a = {x for x in 'abracadabra' if x not in 'abc'}
>>> a
{'r', 'd'}
5.5. 字典 ✔
另一种 Python 中有用的内置数据类型是字典(见Mapping Types — dict)。字典在其他语言中有时被称为联合存储器(associative memories)或者关联数组(“associative arrays”)。字典不像 sequance 那样用一个范围的数字作为索引,而是使用 key 作为索引,key 可以是不可修改的类型;字符串和数字都可以作为 key。元组如果只包含字符串、数字或者元组,就可以作为 key 来使用;元组中如果直接或间接包含可修改的对象,就不能作为 key 来使用。不能用 list 作为字典,因为 list 可以通过索引赋值、切片赋值或者 append() 和 extend() 这样的方法来修改。
最好将字典看作一个键值对的集合,并要求键都是唯一的(在一个字典内)。空字典通过一对花括号({}
)来创建。在花括号中输入用逗号分隔键值对的列表,可以为字典添加初始键值对;这也是字典在输出中打印的方式。
字典中的主要操作是通过 key 来存储 value 和提取 value。可以用 del 删除键值对。如果你用一个已经存在的 key 来存储值,那么原来的 value 会被覆盖。用一个不存在 key 来提取 value 会产生错误。
对字典使用 list(d)
会返回包含字典中所有 key 的列表,列表中元素顺序为插入时的顺序(如果你希望它是被排序过的,就替换成 sorted(d)
)。使用 in 关键字来检查一个 key 是否存在于字典中。
字典的简单示例:
>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel)
['jack', 'guido', 'irv']
>>> sorted(tel)
['guido', 'irv', 'jack']
>>> 'guido' in tel
True
>>> 'jack' not in tel
False
dict()
通过键值对序列来构建字典:
>>> dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
{'sape': 4139, 'guido': 4127, 'jack': 4098}
另外,字典推导式可以通过任意键和值的表达式来创建字典
>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}
当 key 是简单的字符串时,用关键字参数来指定键值对会更加简单:
>>> dict(sape=4139, guido=4127, jack=4098)
{'sape': 4139, 'guido': 4127, 'jack': 4098}
5.6. 循环技巧 ✔
在遍历字典时,使用字典的 items()
方法可以同时将字典中的 key
以及对应的 value
提取出来。
>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
>>> for k, v in knights.items():
... print(k, v)
...
gallahad the pure
robin the brave
当遍历一个 sequence
时,使用 enumerate()
函数可以同时将索引和对应的值提取出来。
>>> for i, v in enumerate(['tic', 'tac', 'toe']):
... print(i, v)
...
0 tic
1 tac
2 toe
想要同时循环两个或多个序列,可以将序列与 zip() 函数一起使用。
>>> questions = ['name', 'quest', 'favorite color']
>>> answers = ['lancelot', 'the holy grail', 'blue']
>>> for q, a in zip(questions, answers):
... print('What is your {0}? It is {1}.'.format(q, a))
...
What is your name? It is lancelot.
What is your quest? It is the holy grail.
What is your favorite color? It is blue.
想要倒序遍历一个 sequence
,可以先指定正向的 sequence
,然后调用 reversed()
函数。
>>> for i in reversed(range(1, 10, 2)):
... print(i)
...
9
7
5
3
1
想要以排好序的顺序遍历一个 sequence
,用 sorted()
函数。该函数返回一个排好序的 list
,保持原始对象不变。
>>> basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
>>> for f in sorted(set(basket)):
... print(f)
...
apple
banana
orange
pear
有时你在遍历一个 list
的过程中,你会很想要改变这个 list
;然而,通常情况下更简单和安全的方式是创建一个新的 list
。
>>> import math
>>> raw_data = [56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8]
>>> filtered_data = []
>>> for value in raw_data:
... if not math.isnan(value):
... filtered_data.append(value)
...
>>> filtered_data
[56.2, 51.7, 55.3, 52.5, 47.8]
5.7. More on Conditions ✔
while
语句和 if
语句中使用的条件可以包含任何运算符,而不仅仅是比较运算符。
比较运算符 in
和 not in
检查一个值是否存在于一个 sequence
中。is
和 is not
操作符比较两个对象是否是相同的对象;这只适用于像 list
这样的可修改对象。所有的比较运算符有相同的优先级,比较运算符的优先级低于数值运算符
比较运算符可以被链接(chained)起来。比如,a < b == c
检查 a
是否小于 b
,且 b
是否等于 c
。
比较运算可以通过布尔运算符 and
和 or
来合并在一起,比较运算(或者其他布尔表达式)的结果可以使用 not
运算符进行否定。and
、or
和 not
的优先级低于比较运算符;在 and
、or
和 not
中,not
的优先级最高,or
最低,所以 A and not B or C
等价于 (A and (not B) or C)
。通常,圆括号可以用于明确所需的组合。
布尔运算符 and 和 or 属于短路(short-circuit)运算符:短路运算符的计算从左往右进行,并且,一旦结果确定,就停止计算。比如,如果 A
和 C
的值为 True
,但 B
为 False
,表达式 A and B and C
就不会再计算 C
。当作为普通值而不是布尔值时,短路运算符的返回值为最后一个计算的操作数。
可以将比较运算或者其他布尔表达式的结果赋值给一个变量。比如:
>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'
Python 与 C 不同,Python 表达式内的赋值必须使用 walrus operator
:=
进行。这能避免在 C 中经常遇到的一类问题:在表达式中本该使用 ==
的地方,却使用了 =
。
5.8. 比较 Sequence 和其他类型
sequence 对象可以与其他具有相同 sequence 类型的对象进行比较。比较通过字典顺序(lexicographical order)进行:首先比较两个序列的第一项,如果不同,那么就可以确定比较结果;如果相同,就比较两个序列的下一项,如此类推,直到其中一个序列的元素被比较完为止。如果进行比较的两个项,本身就是同一类型的 sequence
,则递归地进行字典顺序的比较。如果比较下来,两个序列中所有的项都相等,那么就认为这两个序列相等。如果一个序列是另一个序列的初始子序列(initial sub-sequence),较短的序列是较小的那个。字符串的字典顺序比较使用 Unicode code point number 来排序单个字符。一些同类型序列之间进行比较的例子:
(1, 2, 3) < (1, 2, 4)
[1, 2, 3] < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4) < (1, 2, 4)
(1, 2) < (1, 2, -1)
(1, 2, 3) == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4)
注意,将不同类型的对象进行 <
或 >
比较是合法的,前提是这些对象具有恰当的比较方法。比如,mixed numeric type 通过它们的 numeric value 进行比较,因此 0 等同于 0.0。否则,解释器会抛出一个 TypeError
异常,而不是提供一个任意排序的结果。
脚注
Other languages may return the mutated object, which allows method chaining, such as d->insert("a")->remove("b")->sort();