5. 数据结构:列表、元组、集合与字典
对应官方文档:5. Data Structures 一句话导读:这一章解决「数据一多怎么装、怎么取、怎么排」——把列表、元组、集合、字典四种容器一次捋清,学完能随手写出列表推导式和漂亮的循环。
前四章一直在拿列表和字符串打杂,这一章官方把它们讲深,还补了三件新装备:集合、字典,以及一整套循环技巧。村长严格按 5.1 到 5.8 的编号走,5.1 底下再分五个小节。容器这种东西光看没用,建议边读边把代码敲一遍,手感全靠敲出来。
5.1 深入列表
列表(list)是 Python 里最能干活的容器,官方把方法全摆了出来:append(x) 尾部追加、extend(it) 展开批量追加、insert(i, x) 在 i 前插入、remove(x) 删第一个等于 x 的元素、pop(i=-1) 弹出并返回、clear() 清空、index(x) 查下标、count(x) 数次数、sort() 原地排序、reverse() 原地反转、copy() 浅拷贝。也别什么都往里塞,[None, 'hello', 10] 和 3+4j < 5+7j 排序时都会抛 TypeError。
fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']
print(fruits.count('apple'), fruits.index('banana', 4)) # -> 2 6
fruits.append('grape')
fruits.sort()
print(fruits) # -> ['apple', 'apple', 'banana', 'banana', 'grape', 'kiwi', 'orange', 'pear']
print(fruits.pop(), fruits) # -> pear ['apple', 'apple', ...]
村长划重点:
insert、remove、sort、reverse这些「只改列表」的方法一律返回None,别写x = a.sort()。这是 Python 所有可变数据结构的统一设计:改自己的方法不返回值,只有pop、copy这种要交东西出来的才返回。
5.1.1 把列表当栈用
栈就是「后进先出」:最后放进去的最先拿出来。append() 压栈、不带下标的 pop() 弹栈,天然对得上,末尾操作还特别快。你可以这样理解:它像一摞盘子,只能从顶上放、从顶上拿。
stack = [3, 4, 5]
stack.append(6)
stack.append(7)
print(stack, stack.pop(), stack.pop(), stack.pop()) # -> [3, 4, 5, 6, 7] 7 6 5
print(stack) # -> [3, 4]
5.1.2 把列表当队列用
队列是「先进先出」:先来的先走。列表也能凑合(append() 进、pop(0) 出),但官方明确说这样效率低——从开头删一个元素,后面所有元素都得整体往前挪一格。正经做法是用 collections.deque,它两端进出都是 O(1)。
from collections import deque
queue = deque(['Eric', 'John', 'Michael'])
queue.append('Terry')
queue.append('Graham')
print(queue.popleft(), queue.popleft()) # -> Eric John
print(queue) # -> deque(['Michael', 'Terry', 'Graham'])
村长划重点:队列别用
list.pop(0),数据量大时慢得离谱。记住「栈用 list、队列用 deque」,这是新手和老手的分水岭之一。
5.1.3 列表推导式
想要一个平方数列表,老写法是新建空列表再 for + append,推导式能把它压成一行:[表达式 for 变量 in 可迭代对象],后面还能挂 if 过滤。它不仅短,而且更快、更好读——前提是别写成三层嵌套的绕口令。推导式还有自己的作用域,循环变量不会像普通 for 那样泄漏到外面。
print([x ** 2 for x in range(10)]) # -> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print([x for x in range(20) if x % 3 == 0]) # -> [0, 3, 6, 9, 12, 15, 18]
print([(x, x ** 2) for x in range(4)]) # -> [(0, 0), (1, 1), (2, 4), (3, 9)]
print([s.strip() for s in [' a ', ' b ', ' c ']]) # -> ['a', 'b', 'c']
表达式可以是任意东西,包括函数调用、if 过滤,甚至另一个推导式。
5.1.4 嵌套列表推导式
推导式里再套一个推导式,最常见的是矩阵转置(行变列)。关键是读的顺序:外层 for 先跑,内层 for 在它的上下文里求值,所以 for 子句的顺序和手写嵌套循环完全一致。官方也提醒:现实里别硬写这么绕的,转置用 zip() 一行更香,星号是把 matrix 的行拆开当参数传进去(第 4 章讲过的解包)。
matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
print([[row[i] for row in matrix] for i in range(4)])
# -> [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
print(list(zip(*matrix))) # -> [(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]
5.1.5 列表与推导式中的解包
第 4 章讲过函数调用里用 *seq 拆包,其实造列表时也能拆:给元素前面加 *,就把可迭代对象摊开、逐个塞进新列表。你可以这样理解:像把一整排书从箱子里倒出来,一本一本插到书架上。* 后面必须是可迭代对象,[0, *1, 2] 会抛 TypeError;字典摊开只给出键。
x = [1, 2, 3]
print([0, *x, 4, 5, 6]) # -> [0, 1, 2, 3, 4, 5, 6]
print([*range(3), *'ab']) # -> [0, 1, 2, 'a', 'b']
groups = [[1, 2, 3], [4, 5, 6], [], [7], [8, 9]]
print([y for item in groups for y in item]) # -> [1, 2, 3, 4, 5, 6, 7, 8, 9]
print([y for item in [[1, 2, 3], 'cat', {'spam': 'eggs'}] for y in item])
# -> [1, 2, 3, 'c', 'a', 't', 'spam']
「把一堆列表拍平」时,较新的 Python 版本允许把推导式元素直接写成 [*item for item in groups](官方 5.1.5 就是这么演示的);老解释器不支持这个语法,用上面等价的双层 for 就行。
5.2 del 语句
pop() 按下标删、并把删掉的值还给你;del 则是「按位置抹掉,什么都不返回」。它还能删切片、清空整个列表,甚至把变量名本身删掉——之后再引用就报 NameError。
a = [-1, 1, 66.25, 333, 333, 1234.5]
del a[0]
print(a) # -> [1, 66.25, 333, 333, 1234.5]
del a[2:4]
print(a) # -> [1, 66.25, 1234.5]
del a[:]
print(a) # -> []
del a # 此后 print(a) 会报 NameError
5.3 元组与序列
列表、字符串、元组都属于序列这一大家子,索引、切片、+、*、len()、in 是共通的。元组(tuple)和列表最大的区别就一个字:不可变。还有个冷知识,真正造出元组的是逗号而不是圆括号——输出时 Python 会自动补上括号,好让嵌套元组不歧义。单元素元组必须写 (1,),光写 (1) 只是加了括号的整数;空元组是 ()。序列解包同样适用于列表和元组,左边变量个数必须和右边元素个数对得上。
t = 12345, 54321, 'hello!'
print(t, t[0]) # -> (12345, 54321, 'hello!') 12345
print((), (1,), (1)) # -> () (1,) 1
print((1, 2) + (3, 4), 2 * ('a', 'b'))
try:
t[0] = 88888
except TypeError:
print('元组不可变,赋值会抛 TypeError')
v = ([1, 2, 3], [3, 2, 1]) # 元组里可以装可变对象
print(v, (0, *[1, 2, 3], 4)) # -> ([1, 2, 3], [3, 2, 1]) (0, 1, 2, 3, 4)
5.4 集合
集合(set)是无序、不重复的一堆元素,主打快速判断「在不在」和批量去重,还支持并(|)、交(&)、差(-)、对称差(^)这些数学运算。集合也有推导式。
basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
print('orange' in basket, 'crabgrass' in basket) # -> True False
a, b = set('abracadabra'), set('alacazam')
print(sorted(a - b), sorted(a & b)) # -> ['b', 'd', 'r'] ['a', 'c']
print(sorted(a | b)) # -> ['a', 'b', 'c', 'd', 'l', 'm', 'r', 'z']
print(sorted(a ^ b)) # -> ['b', 'd', 'l', 'm', 'r', 'z']
fruits = [{'apple', 'avocado'}, {'banana', 'blueberry'}]
print(sorted({x for f in fruits for x in f})) # -> ['apple', 'avocado', 'banana', 'blueberry']
官方新版教程里也用 {*item for item in fruits} 演示集合里的解包,上面用等价的双层 for 写,任何 Python 3 都能跑。
村长划重点:空集合只能写
set(),{}是空字典不是空集合!这大概是本章最容易背错的一条。另外集合无序,打印或遍历的顺序不一定和你写进去的一致,别拿它当有序容器用。
5.5 字典
字典(dict)不像序列按数字下标取,而是按键取值,别的语言里叫关联数组或映射。最好把它想成一组「键: 值」对,键必须唯一且不可变:字符串、数字、只含不可变元素的元组都行,列表当不了键(能被原地改)。in 作用在字典上查的是键不是值;存一个已存在的键会静默覆盖旧值,这是很多配置类 bug 的源头。
tel = {'jack': 4098, 'sape': 4139}
tel['guido'] = 4127
print(tel['jack'], 'guido' in tel) # -> 4098 True
del tel['sape']
print(tel, sorted(tel)) # -> {'jack': 4098, 'guido': 4127} ['guido', 'jack']
print(dict(sape=4139, guido=4127, jack=4098))
print({x: x ** 2 for x in (2, 4, 6)}) # -> {2: 4, 4: 16, 6: 36}
odds = {i: i ** 2 for i in (1, 3, 5)}
evens = {i: i ** 2 for i in (2, 4, 6)}
print({**odds, **evens}) # -> {1: 1, 3: 9, 5: 25, 2: 4, 4: 16, 6: 36}
5.6 循环技巧
官方把最常用的「循环姿势」集中在这一节,记住这几个函数,代码立刻短一半:
| 想要什么 | 用什么 | 说明 |
|---|---|---|
| 字典的键和值一起拿 | d.items() | 解包成 k, v |
| 序列的下标和值一起拿 | enumerate(seq) | 比 range(len(seq)) 地道 |
| 多个序列并排遍历 | zip(a, b) | 以最短的为准 |
| 倒着遍历 | reversed(seq) | 返回迭代器 |
| 排好序再遍历 | sorted(seq) | 返回新列表,不改原数据 |
| 去重后再遍历 | set(seq) | 顺手去重 |
knights = {'gallahad': 'the pure', 'robin': 'the brave'}
for k, v in knights.items():
print(k, v) # -> gallahad the pure / robin the brave
print(list(enumerate(['tic', 'tac', 'toe']))) # -> [(0, 'tic'), (1, 'tac'), (2, 'toe')]
print(list(zip(['name', 'quest'], ['lancelot', 'grail'])))
# -> [('name', 'lancelot'), ('quest', 'grail')]
print(list(reversed(range(1, 10, 2)))) # -> [9, 7, 5, 3, 1]
basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
print(sorted(set(basket))) # -> ['apple', 'banana', 'orange', 'pear']
村长划重点:
sorted()和set()都是造新对象,不会动原来的列表;sort()/reverse()才是原地改。想「先去重再排序」写sorted(set(x))一行搞定。另外一边遍历字典一边改它容易出事,稳妥做法是遍历dict(d)或list(d.items())这类副本。
5.7 深入条件
while 和 if 的条件里能用的远不止比较运算符:in / not in 查成员,is / is not 判断是不是同一个对象(不是值相等),比较还能链式写,布尔运算 and / or / not 又有短路特性。
print('a' in 'cat', 'z' not in 'cat') # -> True True
a = [1, 2, 3]
b, c = a, a[:]
print(a is b, a is c, a == c) # -> True False True
print(1 < 2 == 2, 0 < 2 < 5) # -> True True(链式比较)
链式比较 a < b == c 等价于 a < b and b == c,但中间的 b 只求值一次,既短又安全。所有比较运算符优先级相同、且低于算术运算符;and / or 更低,三者里 not 最高、or 最低,所以 A and not B or C 就是 (A and (not B)) or C。短路是重点:and / or 从左到右算,结果一确定就立刻停,而且返回的是「最后被求值的那个操作数」本身,不是硬转成 True / False。
def loud(name):
print('求值了:', repr(name))
return name # 原样返回,方便看清「返回的是操作数本身」
r = loud('') and loud('不会执行') # 左边假,右边跳过;r 是 ''
print('and 结果:', repr(r))
s = loud('Trondheim') or loud('不会执行') # 左边真,右边跳过;s 是 'Trondheim'
print('or 结果:', repr(s))
于是 string1 or string2 or string3 这类写法能用来「取第一个非空值」。还有个和 C 不一样的地方:Python 里表达式中赋值必须用海象运算符 := 显式写,这就杜绝了把 == 手滑打成 = 的经典 bug。
5.8 序列与其他类型的比较
同类型的序列之间可以比大小,规则叫字典序:先比第一对元素,分出胜负就结束;相等就比下一对;如果一方是另一方的前缀,那么短的那个更小。字符串按 Unicode 码点逐字符比,嵌套的序列还会递归地按同样规则比。
print((1, 2, 3) < (1, 2, 4), [1, 2, 3] < [1, 2, 4]) # -> True True
print('ABC' < 'C' < 'Pascal' < 'Python') # -> True
print((1, 2, 3, 4) < (1, 2, 4), (1, 2) < (1, 2, -1)) # -> True True(前缀更短者更小)
print((1, 2, 3) == (1.0, 2.0, 3.0)) # -> True(数值按值比)
print((1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4)) # -> True(递归比较)
不同类型之间用 < / > 比较,只有双方都定义了合适的比较方法时才合法:混合数值类型按数值大小比,所以 0 == 0.0;否则解释器宁愿抛 TypeError,也不会随便编一个顺序出来。注意 == 宽松得多,[1, 2] == (1, 2) 只是 False,并不报错。
本章小结
- 列表方法齐全但「原地修改」类方法一律返回
None;栈用append+pop,队列请用collections.deque的append+popleft。 - 列表推导式
[expr for x in it if cond]短又快,嵌套推导式可读zip(*matrix)替代;*解包能拍平列表,但后面必须是可迭代对象。 del按位置删且不返回值,还能删切片和变量名;元组不可变、逗号才是元组的本体,单元素要写(1,)。- 集合无序去重、空集合写
set();字典按键索引、键必须不可变、in查的是键、覆盖旧值不报错。 - 循环记住
items()、enumerate()、zip()、reversed()、sorted()、set();条件里is比身份、==比值,链式比较与and/or短路能少写很多代码。