《Python编程入门》3.2 列表、元组、字典与集合

本节系统讲解 Python 的四种内建容器:list、tuple、dict、set。先给出有序性、可变性与查找复杂度的对比表,再分别讲透列表的增删改查与带步长的切片、元组的解包与不可变的真实含义、字典的哈希要求与插入有序、集合的去重与交并差运算,并介绍 collections 中的 deque、Counter、defaultdict 与 namedtuple,最后用实测数据说明何时该换容器。

本节目标:读完这一节,你能在一张表里说清 list、tuple、dict、set 的有序性、可变性与查找复杂度差异,能熟练使用列表切片与步长、元组解包、字典的 get / setdefault / | 合并以及集合的交并差运算,并知道何时该把 list 换成 deque 或把 dict 换成 Counter / defaultdict。
适用版本:Python 3.12+(实测 3.14.6)

3.2 列表、元组、字典与集合

上一节我们搞定了「单个值」,但真实程序几乎总是在处理「一组值」:一批订单、一个配置表、一组去重后的用户 ID。Python 内建了四种容器来应对这些需求。它们不是「差不多能用」的替代品——选错容器会让程序慢上几百倍。这一节我们把它们一次讲透。

四种容器一览

先建立全局印象,后面每一节都是对这张表的展开:

容器字面量有序可变元素可重复按下标访问查找复杂度
list[1, 2, 3]是是是O(1)O(n)
tuple(1, 2, 3)是否是O(1)O(n)
dict{"a": 1}是(3.7+)是键唯一按键O(1) 均摊
set{1, 2, 3}否是否否O(1) 均摊

一句话选型:要顺序和下标用 list,要只读用 tuple,要键值映射用 dict,要去重和成员判断用 set。

list:有序可变序列

列表是最常用的容器,增删改查都很直接:

nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(nums[0], nums[-1])     # 首尾元素
print(nums[1:4])             # 下标 1~3
print(nums[:3])              # 前三个
print(nums[-3:])             # 后三个
print(nums[::2])             # 每隔一个取一个(步长)
print(nums[::-1])            # 步长为负则整体反转
3 6
[1, 4, 1]
[3, 1, 4]
[9, 2, 6]
[3, 4, 5, 2]
[6, 2, 9, 5, 1, 4, 1, 3]

增删改各有对应方法,注意 remove 按值删、pop 按位置删并返回:

nums.append(7)          # 尾部追加
nums.insert(0, 0)       # 头部插入
nums.remove(1)          # 删除第一个值为 1 的元素
popped = nums.pop()     # 弹出尾部
nums[0] = 100           # 按下标赋值
print(nums, popped)
[100, 3, 4, 1, 5, 9, 2, 6] 7

切片还能整体赋值,替换一段区间,长度可以不同:

a = [1, 2, 3, 4, 5]
a[1:4] = [20, 30]   # 用两个元素替换三个元素
print(a)
[1, 20, 30, 5]

tuple:不可变的序列

元组和列表长得像,区别是一旦创建就不能修改。尝试赋值会立刻报错:

t = (1, 2, 3)
t[0] = 9
TypeError: 'tuple' object does not support item assignment

不可变带来两个好处:可以当字典的键、可以作为集合元素;语义上也更安全(函数返回「一组固定结果」时不会被人误改)。元组最强大的用法是解包:

point = (3, 4)
x, y = point
print(x, y)                 # 3 4

first, *rest = [1, 2, 3, 4]
print(first, rest)          # 1 [2, 3, 4]

a, b = 1, 2
a, b = b, a                 # 交换,无需临时变量
print(a, b)                 # 2 1

星号 * 还能用在函数调用与字面量里做展开:

print([0, *[1, 2], 3])      # [0, 1, 2, 3]
print({"a": 1, **{"b": 2}}) # {'a': 1, 'b': 2}

dict:键值映射

字典是 Python 里最重要的数据结构,几乎所有对象内部都用它。键必须是可哈希的(即不可变),列表和字典本身不能当键,元组可以:

d = {(1, 2): "tuple key"}   # 元组作键,合法
print(d)
d2 = {[1, 2]: "list"}       # 列表作键,非法
{(1, 2): 'tuple key'}
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')

从 3.7 起字典保证插入有序,遍历顺序就是插入顺序:

d = {"b": 2, "a": 1, "c": 3}
print(list(d.keys()))
d["z"] = 99
print(list(d.keys()))
['b', 'a', 'c']
['b', 'a', 'c', 'z']

| 运算符(3.9 起)用于合并字典,右侧键覆盖左侧:

d1 = {"a": 1, "b": 2}
d2 = {"b": 20, "c": 3}
print(d1 | d2)
d1 |= d2          # 原地更新
print(d1)
{'a': 1, 'b': 20, 'c': 3}
{'a': 1, 'b': 20, 'c': 3}

取值时优先用 get 避免 KeyError;setdefault 则是「不存在才写入」:

cfg = {"host": "localhost"}
print(cfg.get("port"))            # 不存在返回 None
print(cfg.get("port", 8080))      # 给默认值
print(cfg.setdefault("port", 8080))  # 写入并返回
print(cfg.setdefault("port", 9999))  # 已存在,不覆盖
print(cfg)
None
8080
8080
8080
{'host': 'localhost', 'port': 8080}

遍历用 items() 同时拿到键和值:

for k, v in {"x": 1, "y": 2}.items():
    print(k, v)
x 1
y 2

set:去重与集合运算

集合自动去重,成员判断是 O(1),天生适合做「去重」「判断是否包含」「求交集」。需要放进字典当键时,用不可变的 frozenset:

A = {1, 2, 3, 4}
B = {3, 4, 5, 6}
print("并集", A | B)
print("交集", A & B)
print("差集", A - B)
print("对称差", A ^ B)
print("去重", set([1, 1, 2, 2, 3]))
并集 {1, 2, 3, 4, 5, 6}
交集 {3, 4}
差集 {1, 2}
对称差 {1, 2, 5, 6}
去重 {1, 2, 3}

collections 里的四个好帮手

标准库 collections 提供了针对性的增强容器,很多手写循环都能被它们替代。

deque:两端高效的双向队列。列表在头部插入是 O(n),deque 两端都是 O(1):

from collections import deque
dq = deque([1, 2, 3])
dq.appendleft(0)
dq.append(4)
print(dq)
print(dq.popleft(), dq.pop())
deque([0, 1, 2, 3, 4])
0 4

Counter:计数。统计词频、字符出现次数一行搞定:

from collections import Counter
c = Counter("mississippi")
print(c)
print(c.most_common(2))
print(c["s"], c["z"])   # 不存在的键返回 0,不报错
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
[('i', 4), ('s', 4)]
4 0

defaultdict:带默认值的字典,省去每次判断键是否存在:

from collections import defaultdict
dd = defaultdict(list)
for k, v in [("a", 1), ("b", 2), ("a", 3)]:
    dd[k].append(v)
print(dict(dd))
{'a': [1, 3], 'b': [2]}

namedtuple:带字段名的元组,比裸元组可读得多:

from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p, p.x, p.y)
print(p._asdict())
Point(x=3, y=4) 3 4
{'x': 3, 'y': 4}

时间复杂度与选型

选容器本质上是选复杂度。下面这张表是性能优化的第一手依据:

操作listdequedictset
尾部追加O(1)O(1)O(1)O(1)
头部插入O(n)O(1)——
按下标访问O(1)O(n)——
成员判断 inO(n)O(n)O(1)O(1)
删除指定值O(n)O(n)O(1)O(1)

实测一下头部插入的差距(各 10 万次):

import timeit
from collections import deque

N = 100_000
def list_head(n):
    lst = []
    for i in range(n):
        lst.insert(0, i)
    return lst
def deque_head(n):
    dq = deque()
    for i in range(n):
        dq.appendleft(i)
    return dq

print(f"list.insert(0) x{N}: {timeit.timeit(lambda: list_head(N), number=1):.3f}s")
print(f"deque.appendleft x{N}: {timeit.timeit(lambda: deque_head(N), number=1):.4f}s")
list.insert(0) x100000: 2.437s
deque.appendleft x100000: 0.0034s

差了约 700 倍。同样的道理,在 100 万个元素里做成员判断,set 比 list 快到无法比较——前者是常数时间,后者要逐个扫描。当你发现程序在做「列表里找元素」或「列表头部插元素」时,先想想能不能换成 set 或 deque。

小结

四种容器 + 四个增强容器,是日常 Python 的全部武器库:

  • list 有序可变、支持切片与步长;tuple 有序不可变、擅长解包,可作字典键。
  • dict 的键必须可哈希,3.7 起保证插入有序,| 合并、get / setdefault 是高频操作。
  • set 自动去重、成员判断 O(1),支持交并差对称差;frozenset 可作键。
  • deque 两端 O(1),Counter / defaultdict / namedtuple 能替掉大量手写循环。
  • 选型看复杂度:列表头部插入与成员判断是两大性能陷阱。

容器里的元素到底是怎么被「共享」和「复制」的?为什么把列表传给函数后原列表会被改掉?下一节 3.3 可变与不可变、引用语义与拷贝 会揭开 Python 对象模型的这一层,它是避免整类诡异 bug 的关键。

阅读导航:上一节:3.1 数值、字符串与 f-string 格式化 · 下一节:3.3 可变与不可变、引用语义与拷贝 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时