跳到主要内容

数据结构进阶

列表、字典、字符串是 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。这种设计有三大好处:

  1. 容易计算长度s[a:b] 的长度就是 b - a
  2. 容易分断s[:3]s[3:] 正好把序列分成两段,不重复不遗漏
  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 类型最小字节数
bsigned charint1
Bunsigned charint1
uwchar_tUnicode2
hsigned shortint2
Hunsigned shortint2
isigned intint2
Iunsigned intint2
lsigned longint4
Lunsigned longint4
ffloatfloat4
ddoublefloat8

双向队列 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 类似的操作(索引、切片、splitstartswith 等),但参数必须是 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'

如果需要同时处理文本和字节,reos 模块的一些函数支持双模式:

>>> import os
>>> os.listdir('.') # 传入 str,返回 str
>>> os.listdir(b'.') # 传入 bytes,返回 bytes

小结

  • 列表推导和生成器表达式让代码更简洁、更高效
  • 元组既可以当记录(位置有意义),也可以当不可变列表
  • deque 在两端操作比列表快得多
  • defaultdictCounterOrderedDict 让字典更强大
  • 集合支持数学运算,去重效率高
  • Python 3 严格区分 str(Unicode)和 bytes(原始字节)
  • 读写文件时显式指定 encoding='utf-8'