5. 数据结构

IamZS 95 0

本章将详细讲解一些之前已经学过的知识,并补充一些新的内容。

5.1. 列表再深入

列表数据类型还有更多的方法。下面是列表对象所有的方法:

list.append(x)

将一个元素添加到列表末尾。相当于:a[len(a):] = [x]

list.extend(iterable)

将所有可迭代对象中的元素添加到列表末尾。相当于:a[len(a):] = iterable

list.insert(i, x)

将一个元素插入到指定位置。第一个参数为被插入元素的位置索引,所以 a.insert(0, x) 表示将 x 插入到列表第一个,a.insert(len(a), x) 就相当于 a.append(x)

list.remove(x)

将列表中第一个值为 x 的元素剔除。如果不存在这样的元素,则会报 ValueError 的错。

list.pop([i])

删除列表中指定位置的元素并返回它。如果不存在该索引,a.pop() 移除并返回列表中的最后一项。(i 两边的方括号表示该参数是可选的,而不是让你在该位置输入方括号。你将会在 Python 参考库中经常看到这种表示方法。

list.clear()

移除列表中的所有元素。相当于:del a[:]

list.index(x[, start[, end]])

返回列表中第一个值为 x 的元素的索引(索引都是从 0 开始)。如果不存在,则会报 ValueError 的错误。

可选参数 start 和 end 以切片的形式被解析,用于限制在特定的列表子序列中进行搜索。返回的索引是相对于整个列表来说的,与 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'

你可能已经注意到像 insertremovesort 它们仅仅修改列表而没有返回值打印出来——它们返回的是默认的 None。这是 Python 中所有可变数据结构的设计原则。

5.1.1. 列表作为栈使用

列表的方法使其作为栈使用非常简单,最后一个添加的元素是第一个取出的元素(也就是“后进先出”)。要将某一项添加到栈的顶端,使用 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. 列表作为队列使用

也可以将列表作为队列来使用,第一个添加的元素是第一个取出的元素(也就是“先进先出”);然而,列表作为队列使用并不高效。从列表末尾添加和移除元素非常快,但是往列表头部插入或移除元素却非常慢(因为所有其它元素都必须向右或向左移动一位)。、

要实现一个队列,请使用 collections.deque,它被设计成可以快速地从两端进行插入和移除元素操作。比如:

>>> 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 Comprehensions)

列表推导式提供了一种创建列表的简洁方式。公共程序(common applications)用于生成新的列表,其中的每一项都是对另一个子序列或可迭代对象中的元素进行某种操作后得到的结果,或者是创建这些元素中满足特定条件的子序列。

例如,我们想创建一个平方数列表,可以这样:

>>> 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 子句或者是 if 子句。其结果是一个新的列表,它的值是将表达式放在 for 和 if 子句上下文中得到的结果。比如,下面这个 listcomp 将两个列表中不相等的元素组合在一起:

>>> [(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 语句的顺序是如何保持一致的。

如果表达式是一个元组(就像前面示例中的 (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. 嵌套列表推导式

列表推导式中的初始表达式可以是任何表达式,包括另一个列表推导式。

考虑一下下面这个由 3 个长度为 4 的列表组成的 3x4 矩阵:

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

正如我们前一节看到的,嵌套的列表推导式(listcomp)是在其后的 for 语句上下文中进行求值,因此这个例子就相当于:

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

参见解包参数列表中关于这行中星号的详细介绍。

5.2. 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 进行引用将会报错(至少在对 a 再次赋值前)。我们之后将发现 del 的其他用法。

5.3. 元组和序列

我们看到,列表和字符串有很多共同的属性,比如索引和切片操作。它们是序列数据类型(参见序列类型——list、tuple、range)的两个例子。因为 Python 是一门不断演进的语言,其他序列数据类型可能会被添加进来。另一种标准序列数据类型就是:元组。

元组由一系列用逗号分隔的值组成,比如:

>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples may be nested:
... u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples are immutable:
... t[0] = 88888
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # but they can contain mutable objects:
... v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])

正如你所见,在输出中,元组总是位于括号中,所以嵌套的元组可以被正确解析;输入的时候它们周围可以有也可以没有括号,尽管括号经常是必要的(如果元组是更大的表达式的一部分)。不能给元组中单独的某个元素赋值,不过可以创建包含可变对象(例如列表)的元组。

尽管元组看上去类似于列表,但它们经常被用于不同的场景和目的。元组是不可变的,经常包含不同类型的元素,这些元素通过解包(参见本节后面部分)或索引(甚至通过命名元组(namedtuples)的属性)进行访问。列表是可变的,它们元素的类型通常是相同的,通过迭代列表来访问这些元素。

那些只包含 0 个或 1 个元素的元组比较特殊,在表示语法上有些不同。空元组用一对括号来表示;只包含一个元素的元组通过元素后加一个逗号的方式来表示(仅用一对括号把这个值括起来是不够的)。这种表示方法看上去很丑,但是有效。比如:

>>> empty = ()
>>> singleton = 'hello',    # <-- note trailing comma
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)

语句 t = 12345, 54321, 'hello!' 是元组封装的一个例子:值 1234554321 和 'hello!' 被一起封装在一个元组中。其逆操作也是可以的:

>>> x, y, z = t

这被称为序列解包(sequence unpacking),适用于右边的任何序列。序列解包要求等号左边变量的个数和等号右边序列中的元素数量相等。注意:多重赋值只是将元组封装和序列解包结合起来使用。

5.4. 集合

Python 还包含了一种数据类型——集合。集合中的元素是无序且不重复的。它的基本用途包括成员测试(membership testing)和消除重复条目(eliminating duplicate entries)。集合还支持数学运算,如并(union)、交(intersection)、差(difference)和对称差(symmetric difference)。

大括号或 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'}

类似于列表推导式,也支持集合推导式:

>>> a = {x for x in 'abracadabra' if x not in 'abc'}
>>> a
{'r', 'd'}

5.5. 字典

另一种有用的 Python 内建数据类型是字典(参见映射类型——字典)。在其他语言中字典有时候被称为关联记忆(associative memories)或关联数组(associative arrays)。与通过数字作为索引的序列不同,字典通过键(keys)进行索引,键可以是任何不可变类型;字符串和数字总是可以作为键使用。那些只包含字符串、数字或元组的元组也可以作为键使用;如果一个元组直接或间接地包含任何可变对象,那么它不能作为键使用。不可以将列表作为键使用,因为可以通过索引赋值、切片赋值或者 append() 和 extend() 等方法对列表进行修改。

最好将字典看作是键值对集合,一个字典中的键必须是唯一的。一对大括号创建一个空的字典:{}。把用逗号分隔的键值对序列放在大括号中将初始键值对添加到字典中;这也是字典作为输出时的表示方式。

对字典的主要操作就是根据键来存储值。还可以通过 del 来删除一个键值对。如果使用一个已经使用过的键来存值,原有与该键相关联的值便会被覆盖(forgotten)。使用一个不存在的键来取值的时候会报错。

对字典使用 list(d) 将会以列表的形式返回字典中所有的键,键的顺序为插入时的顺序(如果想对它进行排序,使用 sorted(d))。要检查某个键是否在字典中,可以使用 in 关键字。

下面是一个使用字典的例子:

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

当键都是简单字符串时,用关键字参数来指定键值对有时候更简单:

>>> dict(sape=4139, guido=4127, jack=4098)
{'sape': 4139, 'guido': 4127, 'jack': 4098}

5.6. 循环技巧

在循环遍历字典时,可以使用 items() 方法同时将键和与之对应的值取出来。

>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
>>> for k, v in knights.items():
...     print(k, v)
...
gallahad the pure
robin the brave

当循环遍历一个序列时,可以使用 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.

要反向循环遍历一个序列,首先正向生成一个序列然后调用 reversed() 函数。

>>> for i in reversed(range(1, 10, 2)):
...     print(i)
...
9
7
5
3
1

要按顺序循环遍历一个序列,可以使用 sorted() 函数,它会返回一个排好序后的列表,同时原列表保持不变。

>>> basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
>>> for f in sorted(set(basket)):
...     print(f)
...
apple
banana
orange
pear

如果想在遍历一个列表的同时对其进行修改,创建一个新的列表通常更简单和安全。

>>> 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. 深入条件控制

whileif 语句中的条件可以包含任何操作符,而不仅仅是比较操作符。

比较操作符 innot in 检查某个值是否出现在序列中。操作符 isis not 比较两个对象是否是相同对象;这只对像列表这类的可变对象来说比较重要。所有比较运算符的优先级都相同,同时低于低于数值运算符的优先级。

可以进行级联比较(comparisons can be chained)。比如,a < b == c 测试是否 a 小于 b 同时 b 等于 c

可以使用布尔运算符 andor 将比较运算组合起来,比较的结果(或是任何其他布尔表达式)可以使用 not 进行取反。这些运算符的优先级低于比较操作符;在这三者之间,not 的优先级最高,or 的优先级最低,因此 A and not B or C 相当于 (A and (not B)) or C。和平常一样,括号可以被用于表达想要的组合。

布尔运算符 andor 被称作短路操作符:按照参数顺序从左到右求值,结果一旦确定就停止。例如,如果 ACTrue,但是 BFalse,那么 A and B and C 将不会计算到表达式 C 就停止。当被用作普通值而非布尔值时,短路操作符返回的值通常是最后一个被计算的参数。

可以将一个比较运算或是其他布尔表达式的返回值赋值给一个变量。比如:

>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'

注意:Python 和 C 语言不同,赋值不能出现在表达式内部。C 语言程序员可能会抱怨这一点,但这避免了 C 语言程序中常见的一类问题:输入 = 而真正的意图是 ==

5.8. 序列和其他类型的比较

序列对象可以和相同类型的其他序列对象进行比较。按照字典顺序来进行比较:首先比较两个序列的首元素,如果不同,就决定了比较的结果;如果相同,就比较后面两个元素,以此类推,直到其中一个序列穷举完。如果要比较的两个元素本身就是同一类型的序列,就按照字典序递归比较。如果两个序列的所有元素都相等,那么就认为这两个序列相等。如果一个序列是另一个序列的初始子序列,较短的序列就小于另一个。字符串的字典序根据 Unicode 编码顺序对单个字符进行排序。下面是同类型序列之间比较的一些例子:

(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)

注意:使用 < 或 > 比较不同类型的对象也是合法的,只要对象具有恰当的比较方法。比如,不同的数字类型按照它们的数值大小进行比较,所以 0 等于 0.0,等等。否则,解释器将会报 TypeError 的异常,而不是随便给出一个顺序。

发表评论
表情 图片 链接 代码