数据结构进阶
列表、字典、字符串是 Python 中最常用的数据结构。本章深入探讨这些类型的进阶用法,以及一些标准库中更专业的数据结构。
列表推导与生成器表达式
列表推导是 Python 的标志性特性之一,它比 map/filter 更直观:
# 传统方式
squares = []
for x in range(10):
squares.append(x**2)
# 列表推导
squares = [x**2 for x in range(10)]
带条件过滤:
>>> [x for x in range(20) if x % 3 == 0]
[0, 3, 6, 9, 12, 15, 18]
多层循环:
>>> [(x, y) for x in [1, 2] for y in ['A', 'B']]
[(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')]
注意:嵌套过多的列表推导会降低可读性,超过两层建议用普通循环。
生成器表达式
如果只需要迭代而不需要保存结果,用生成器表达式更省内存:
>>> sum(x**2 for x in range(1000000)) # 圆括号,不是方括号
333333833333500000
元组不仅仅是不可变列表
元组作为记录
元组的第一种用法是当作记录(record)——位置和含义绑定的数据结构:
>>> member = ('Alice', 'CS', 3) # (名字, 专业, 年级)
>>> name, major, year = member # 拆包
>>> name
'Alice'
具名元组
用 collections.namedtuple 可以给元组的每个位置起名字:
>>> from collections import namedtuple
>>> Member = namedtuple('Member', ['name', 'major', 'year'])
>>> alice = Member('Alice', 'CS', 3)
>>> alice.name
'Alice'
>>> alice[0] # 仍然可以用索引
'Alice'
具名元组比普通类更省内存,而且不可变,适合存储简单的结构化数据。
作为不可变列表
元组的第二种用法是当作不可变的列表。由于不可变,元组可以作为字典的键或集合的元素:
>>> locations = {
... (0, 0): '起点',
... (1, 0): '路口',
... }
>>> locations[(0, 0)]
'起点'
切片
切片是 Python 序列类型的核心操作,但很多人只掌握了基础用法:
>>> nums = [10, 20, 30, 40, 50]
>>> nums[1:3] # [20, 30]
>>> nums[:3] # [10, 20, 30]
>>> nums[::2] # [10, 30, 50] 步长为2
>>> nums[::-1] # [50, 40, 30, 20, 10] 反转
为什么切片忽略最后一个元素
Python 的切片 s[a:b] 包含 a,不包含 b。这种设计有三大好处:
- 容易计算长度:
s[a:b]的长度就是b - a - 容易分断:
s[:3]和s[3:]正好把序列分成两段,不重复不遗漏 - 空切片直观:
s[:0]是空列表,s[-0:]也是空列表
给切片赋值
列表支持切片赋值,甚至可以改变长度:
>>> nums = [1, 2, 3, 4, 5]
>>> nums[1:3] = [20, 30, 40] # 替换并扩展
>>> nums
[1, 20, 30, 40, 4, 5]
>>> nums[1:4] = [] # 删除
>>> nums
[1, 4, 5]
数组
如果列表里全是数字,用 array.array 更省内存:
>>> from array import array
>>> nums = array('d', [1.0, 2.5, 3.7]) # 'd' 表示双精度浮点
>>> nums.append(4.2)
>>> nums[0]
1.0
array 存储的是原始的二进制值,而不是 Python 对象引用,所以比列表更紧凑。
常用类型码:
| 类型码 | C 类型 | Python 类型 | 最小字节数 |
|---|---|---|---|
b | signed char | int | 1 |
B | unsigned char | int | 1 |
u | wchar_t | Unicode | 2 |
h | signed short | int | 2 |
H | unsigned short | int | 2 |
i | signed int | int | 2 |
I | unsigned int | int | 2 |
l | signed long | int | 4 |
L | unsigned long | int | 4 |
f | float | float | 4 |
d | double | float | 8 |
双向队列 deque
列表在头部插入/删除元素效率很低(O(n))。如果需要频繁在两端操作,用 collections.deque:
>>> from collections import deque
>>> dq = deque([1, 2, 3])
>>> dq.append(4) # 右侧添加
>>> dq.appendleft(0) # 左侧添加
>>> dq
deque([0, 1, 2, 3, 4])
>>> dq.pop() # 右侧弹出
4
>>> dq.popleft() # 左侧弹出
0
deque 还可以设置最大长度,实现 LRU 缓存:
>>> dq = deque(maxlen=3)
>>> dq.extend([1, 2, 3])
>>> dq.append(4)
>>> dq
deque([2, 3, 4]) # 1 被挤出去了
字典进阶
字典推导
和列表推导类似:
>>> names = ['Alice', 'Bob', 'Carol']
>>> name_lengths = {name: len(name) for name in names}
>>> name_lengths
{'Alice': 5, 'Bob': 3, 'Carol': 5}
defaultdict
collections.defaultdict 可以在键不存在时自动创建默认值:
>>> from collections import defaultdict
>>> groups = defaultdict(list)
>>> groups['算法'].append('Alice')
>>> groups['算法'].append('Bob')
>>> groups['前端'].append('Carol')
>>> dict(groups)
{'算法': ['Alice', 'Bob'], '前端': ['Carol']}
用 setdefault 也能实现类似效果,但代码更啰嗦:
>>> groups = {}
>>> groups.setdefault('算法', []).append('Alice')
Counter
collections.Counter 是计数器的专用字典:
>>> from collections import Counter
>>> votes = Counter(['Alice', 'Bob', 'Alice', 'Carol', 'Alice', 'Bob'])
>>> votes
Counter({'Alice': 3, 'Bob': 2, 'Carol': 1})
>>> votes.most_common(2)
[('Alice', 3), ('Bob', 2)]
OrderedDict
Python 3.7+ 的普通字典已经保证插入顺序,但 collections.OrderedDict 提供了一些额外的方法,如 move_to_end:
>>> from collections import OrderedDict
>>> d = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>> d.move_to_end('a') # 把 'a' 移到最后
>>> list(d.keys())
['b', 'c', 'a']
集合
集合支持数学上的并集、交集、差集等操作:
>>> a = {1, 2, 3, 4}
>>> b = {3, 4, 5, 6}
>>> a | b # 并集
{1, 2, 3, 4, 5, 6}
>>> a & b # 交集
{3, 4}
>>> a - b # 差集
{1, 2}
>>> a ^ b # 对称差集(只在其中一个集合中的元素)
{1, 2, 5, 6}
集合推导:
>>> {x for x in range(10) if x % 2 == 0}
{0, 2, 4, 6, 8}
frozenset
集合是可变的,不能作为字典的键。frozenset 是不可变的集合,可以哈希:
>>> fs = frozenset([1, 2, 3])
>>> {fs: "不可变集合"}
{frozenset({1, 2, 3}): '不可变集合'}
文本与字节序列
字符与字节
Python 3 严格区分字符串(str,Unicode 字符序列)和字节序列(bytes,原始字节):
>>> s = 'café' # 4 个 Unicode 字符
>>> b = s.encode('utf-8') # 编码为 5 个字节(é 占 2 字节)
>>> b
b'caf\xc3\xa9'
>>> b.decode('utf-8')
'café'
编码与解码
字符串 → 字节:encode()
字节 → 字符串:decode()
>>> text = "你好"
>>> utf8 = text.encode('utf-8')
>>> gbk = text.encode('gbk')
>>> len(utf8), len(gbk)
(6, 4)
常见的编码问题:
>>> b'café'.decode('ascii') # é 不在 ASCII 范围内
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3
处理文本文件
打开文本文件时,Python 默认使用 UTF-8(Python 3.15 起)或系统编码:
# 明确指定编码,避免跨平台问题
with open('data.txt', 'r', encoding='utf-8') as f:
text = f.read()
with open('output.txt', 'w', encoding='utf-8') as f:
f.write(text)
最佳实践:所有文本文件都显式指定 encoding='utf-8',这样代码在任何平台上表现一致。
bytes 的基本操作
bytes 支持很多和 str 类似的操作(索引、切片、split、startswith 等),但参数必须是 bytes 而不是 str:
>>> b = b'hello world'
>>> b[:5]
b'hello'
>>> b.split(b' ')
[b'hello', b'world']
>>> b.replace(b'hello', b'hi')
b'hi world'
如果需要同时处理文本和字节,re 和 os 模块的一些函数支持双模式:
>>> import os
>>> os.listdir('.') # 传入 str,返回 str
>>> os.listdir(b'.') # 传入 bytes,返回 bytes
小结
- 列表推导和生成器表达式让代码更简洁、更高效
- 元组既可以当记录(位置有意义),也可以当不可变列表
deque在两端操作比列表快得多defaultdict、Counter、OrderedDict让字典更强大- 集合支持数学运算,去重效率高
- Python 3 严格区分
str(Unicode)和bytes(原始字节) - 读写文件时显式指定
encoding='utf-8'