本章

基础篇 / 第 5 章

第 5 章 · 内置数据结构全景

本章导读:list、tuple、set、dict 是 Python 处理数据的主力武器,选对容器类型往往比优化算法更有效。本章系统讲解四种内置容器的语义、增删改查方法、遍历方式、彼此之间的转换、浅拷贝与深拷贝的陷阱,以及各自的性能特征与选型依据。前置知识:会索引切片(第 4 章)、会写循环(第 3 章)。学完本章,你能面对任何数据需求立刻说出「该用哪个容器、怎么写最快」。

四种容器的全景对比

先建立整体认知。下表是本章最重要的一张表,建议打印下来贴在显示器边上。

特性list 列表tuple 元组set 集合dict 字典
中文名列表元组集合字典
语法[1, 2](1, 2){1, 2}{1: 2}
有序是是是(3.7+ 保留插入顺序)是(3.7+ 保留插入顺序)
可重复是是否键不可重复
可修改是否是是
索引访问seq[i] O(1)seq[i] O(1)不支持d[k] O(1)
按内容查找O(n)O(n)O(1) 哈希O(1) 哈希
可哈希(做字典键)否是(元素须可哈希)否否
典型用途有序可变序列不可变记录、多返回值去重、集合运算键值映射、查找表

一个「选型决策」速查

# 数据需要按顺序、且会增删吗?        -> list
items = ["苹果", "香蕉", "橙子"]

# 数据是固定的记录(如坐标、日期),不希望被改吗? -> tuple
point = (3, 5)
rgb = (255, 128, 0)

# 只需要「有没有」,且要去重?                -> set
tags = {"python", "web", "data"}
visited = {user_id for user_id in all_ids}

# 需要「名字 -> 值」的快速查找?            -> dict
scores = {"张三": 89, "李四": 76}
要点选择原则:能用 tuple 就不用 list(不可变更安全)、能用 dict/set 就不用 list(哈希查找是 O(1),列表是 O(n))。数据量大时这一点能带来数量级的性能差距。

列表 list:有序可变序列

2.1 创建与基本操作

列表的增删改查

fruits = ["苹果", "香蕉", "橙子"]

# 查
print(fruits[0])           # 苹果
print(fruits[-1])          # 橙子
print(fruits[1:3])         # ['香蕉','橙子']
print(len(fruits))         # 3
print("香蕉" in fruits)     # True
print(fruits.index("橙子")) # 2

# 增
fruits.append("葡萄")              # 末尾追加
fruits.insert(1, "梨")             # 在索引 1 处插入
fruits.extend(["西瓜", "桃"])       # 批量追加(注意:加的是元素本身)
print(fruits)
# ['苹果','梨','香蕉','橙子','葡萄','西瓜','桃']

fruits += ["草莓"]                 # += 对列表是原地扩展
print(fruits[-1])          # 草莓

# 删
fruits.remove("梨")               # 按值删(删第一个)
last = fruits.pop()               # 弹出最后一个,返回被删元素
second = fruits.pop(1)            # 弹出指定位置(第 1 个索引)
fruits.clear()                     # 清空整个列表
print(fruits)               # []

# 改
nums = [1, 2, 3, 4, 5]
nums[0] = 100               # 按索引赋值
print(nums)                 # [100, 2, 3, 4, 5]

# 也可以用切片批量修改(长度必须匹配)
nums[1:3] = [20, 30]
print(nums)                 # [100, 20, 30, 4, 5]
常见坑append 和 extend 完全不同:append(["a","b"]) 追加的是「一个列表元素」,extend(["a","b"]) 追加的是「两个元素」。想一次加多个用 extend 或 +=。

2.2 常用列表方法

方法作用示例
append(x)末尾追加一个元素[1].append(2) → [1,2]
insert(i,x)在位置 i 插入[1,3].insert(1,2) → [1,2,3]
extend(lst)批量追加[1].extend([2,3]) → [1,2,3]
remove(x)按值删除找不到会报 ValueError
pop([i])弹出并返回不给索引则弹末尾
index(x)首次出现位置找不到报 ValueError
count(x)出现次数[1,1].count(1) → 2
sort()原地排序(无返回值)l.sort()
reverse()原地反转l.reverse()
copy()浅拷贝l.copy()
clear()清空l.clear()
常见坑sort() 返回 None!写 b = a.sort() 会让 b 变成 None,而 a 被就地修改了。要「返回新列表」应该用 sorted(a)。

2.3 列表排序

sort 与 sorted 的四种排序方式

nums = [3, 1, 4, 1, 5, 9, 2, 6]

# 1. 原地升序排序
nums.sort()
print(nums)                     # [1,1,2,3,4,5,6,9]

# 2. 原地降序
nums.sort(reverse=True)
print(nums)                     # [9,6,5,4,3,2,1,1]

# 3. sorted() 返回新列表
print(sorted(nums))             # 升序新列表
print(sorted(nums, reverse=True))   # 降序新列表
# 注意 sorted 不修改原列表

# 4. 按 key 排序(最常用)
words = ["banana", "apple", "cherry", "date"]
print(sorted(words, key=len))             # 按长度:['date', ...]
print(sorted(words, key=str.upper))       # 不区分大小写

# 多条件排序:先按 A 再按 B
users = [
    {"name": "张三", "age": 25, "city": "北京"},
    {"name": "李四", "age": 20, "city": "上海"},
    {"name": "王五", "age": 25, "city": "广州"},
]
# 先按年龄升序,再按城市升序
result = sorted(users, key=lambda u: (u["age"], u["city"]))
for u in result:
    print(u["name"], u["age"], u["city"])
# 李四 20 上海
# 王五 25 广州
# 张三 25 北京

# 原地排序配合 key
nums2 = [-3, 5, -1, 9]
nums2.sort(key=abs)            # 按绝对值排:-3 -1 5 9
print(nums2)
技巧sorted(数据, key=lambda x: x["字段"], reverse=True) 是处理结构化数据最常见的排序姿势。key 只返回一个「排序依据」,可以随意复杂。

2.4 列表的常用操作技巧

不写循环也能完成的操作

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 反转
print(nums[::-1])              # [10,9,...,1]
nums.reverse()                 # 原地反转

# 取偶数位置的元素(下标 0,2,4...)
print(nums[::2])

# 求和 / 最大 / 最小 / 长度
print(sum(nums), max(nums), min(nums), len(nums))

# 排序后取前 N 大
top3 = sorted(nums, reverse=True)[:3]
print("前三:", top3)

# 列表去重(保持顺序)
names = ["张三", "李四", "张三", "王五", "李四"]
unique_names = list(dict.fromkeys(names))    # 保持插入顺序
print(unique_names)             # ['张三','李四','王五']

# 另一个去重方法:借助集合(但不保证顺序)
print(list(set(names)))         # 顺序不保证

# 列表与字符串互转
chars = list("hello")           # ['h','e','l','l','o']
text = "".join(chars)           # "hello"
print(chars, text)

# 二维列表
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [num for row in matrix for num in row]    # 展平
print(flat)                     # [1,2,3,4,5,6]

# 列表推导式创建
squares = [i ** 2 for i in range(1, 11)]
evens = [i for i in squares if i % 2 == 0]
print(squares, evens)

元组 tuple:不可变序列

3.1 元组的特点

元组与列表的对照

# 创建
t = (1, 2, 3)                  # 标准写法,逗号不能省
t2 = 1, 2, 3                   # 也算元组(逗号决定的)
t3 = (1,)                      # 单元素元组必须有逗号!
# t4 = (1)                   # 这是整数 1,不是元组!
t5 = ()                        # 空元组

print(type(t), type(t2), type(3), type(t3), type(t5))
# <class 'tuple'> <class 'tuple'> <class 'int'> <class 'tuple'> <class 'tuple'>

# 元组不可变
t = (1, 2, 3)
# t[0] = 99                  # TypeError
# t.append(4)                # AttributeError(没有这个方法)

# 有哪些方法
print(dir(t))
# 常用:count()、index()、count(x)、index(x)
print(t.count(1))              # 1
print(t.index(2))              # 1

# 索引、切片、遍历和列表一样
print(t[0], t[1:], len(t))
for x in t:
    print(x)
常见坑(1) 不是元组而是整数,(1,) 才是单元素元组。「什么时候需要逗号」是新手最常踩的坑之一。

3.2 元组最重要的用途:多返回值与不可变记录

元组的三大实战场景

# 1. 多返回值(Python 的招牌功能)
def min_max(numbers):
    return min(numbers), max(numbers)        # 返回的其实是元组

lo, hi = min_max([3, 1, 4, 1, 5])
print(lo, hi)        # 1 5

# 2. 交换变量
a, b = 1, 2
a, b = b, a           # 底层就是元组解包

# 3. 不可变记录:适合当字典的键
locations = {
    (0, 0): "原点",
    (3, 4): "五三组合",
}
print(locations[(3, 4)])       # 五三组合  ← 键是元组

# 元组作字典键是合法的(可哈希),列表就不行
# 4. 作为「固定的配置」
TIMEOUT = (3, 5)               # (连接超时, 读取超时)
conn_timeout, read_timeout = TIMEOUT

# 5. 嵌套结构:坐标、RGB、日期
point = (10, 20)
color = (255, 128, 0)           # RGB
matrix3d = ((1, 2), (3, 4))     # 嵌套元组

# 6. 元组 vs 列表:什么时候必须用哪个?
#    - 用列表:数据会变化、需要增删
#    - 用元组:数据固定、想防止误改、可哈希
IMMUTABLE = ("red", "green", "blue")    # 颜色常量用元组更安全
要点「浅不可变」提醒:t = ([1], 2) 中元组本身不可变,但里面的列表元素可变,t[0].append(3) 合法。元组的不可变只保护「引用不变」,不保护「对象内容不变」。

集合 set:去重与集合运算

4.1 集合的创建与去重

集合的自动去重

# 创建(注意大括号)
s = {1, 2, 3}                  # 集合字面量
s2 = {1, 2, 3, 2, 1, 1}        # 重复的自动去掉
print(s2)                      # {1, 2, 3}

empty = set()                  # 空集合(不能用 {},那是空字典)
print(type({}))                # <class 'dict'>
print(type(set()))             # <class 'set'>

# 从可迭代对象创建
print(set([1, 2, 2, 3]))       # {1, 2, 3}
print(set("hello"))            # {'h','e','l','o'}  字符去重
print(set((1, 2)))             # {1, 2}

# 集合推导式(去重的集合字面量不能用)
squares = {i ** 2 for i in range(5)}
print(squares)                 # {0, 1, 4, 9, 16}
常见坑{} 是空字典不是空集合!set() 才是空集合。这是新手最高频的集合错误。

4.2 集合的增删与判断

集合方法

s = {1, 2, 3}

# 增
s.add(4)                       # 添加单个元素
s.update([5, 6])               # 批量添加
s.discard(6)                   # 删除(不存在也不报错)
s.remove(99)                   # 删除(不存在报 KeyError)
print(s)                       # {1,2,3,4,5}

# 查
print(len(s))
print(2 in s)                  # O(1) 哈希查找
print(99 in s)                 # False

# 拷贝
s2 = s.copy()
s2.add(100)
print(s, s2)                   # 两个独立集合

# 清空
s.clear()
print(s)                       # set()

4.3 集合运算:数学里的交集并集差集

运算数学方法运算符含义
交集A ∩ Ba & b 或 a.intersection(b)&两边都有的
并集A ∪ Ba | b 或 a.union(b)|合并去重
差集A - Ba - b 或 a.difference(b)-在 A 不在 B
对称差A △ Ba ^ b 或 a.symmetric_difference(b)^只在一边的
子集A ⊆ Ba.issubset(b)<=A 是 B 的子集
真子集A ⊂ Ba < b<A 是 B 的真子集
超集A ⊇ Ba.issuperset(b)>=B 是 A 的子集
不相交—a.isdisjoint(b)—无共同元素

集合运算实战

python_skills = {"语法", "面向对象", "Web", "数据分析"}
java_skills = {"语法", "面向对象", "JVM", "并发"}

# 交集:两种技能都有
print(python_skills & java_skills)
# {'语法', '面向对象'}
print(python_skills.intersection(java_skills))     # 方法写法

# 并集:所有技能
print(python_skills | java_skills)
# {'语法','面向对象','Web','数据分析','JVM','并发'}

# 差集:只有 Python 有的
print(python_skills - java_skills)
# {'Web', '数据分析'}

# 对称差集:各自独有的
print(python_skills ^ java_skills)
# {'Web', '数据分析', 'JVM', '并发'}

# 关系判断
all_skills = {"语法", "Web"}
print(all_skills.issubset(python_skills))          # True
print(all_skills <= python_skills)               # True

# 实际用途:找重复注册的邮箱、找两个文件的差异行
list_a = [1, 2, 3, 4]
list_b = [3, 4, 5, 6]
common = set(list_a) & set(list_b)              # 交集:{3,4}
only_a = set(list_a) - set(list_b)                # 差集:{1,2}
print("共同:", common, "独有A:", only_a)

# 集合是可变且无序,不要依赖其顺序
要点集合运算在数据量相等时,无论元素多少都近似 O(1) 完成(底层是哈希运算)。这就是「用集合代替列表做存在性判断」能快几百倍的原因。

字典 dict:键值映射

5.1 字典的创建与增删改查

字典基本操作

# 创建
d = {}                                  # 空字典
d = {"name": "张三", "age": 18}         # 字面量
d = dict(name="张三", age=18)           # 构造函数
d = {"a": 1, **other_dict}             # 合并(3.5+)

# 查
print(d["name"])                # 张三    键不存在报 KeyError
print(d.get("name"))            # 张三
print(d.get("phone", "无"))     # 有默认值,不报错 ← 最常用
print(d.get("height"))          # None(键不存在返回 None)
# print(d["phone"])            # KeyError

# 增 / 改
d["city"] = "北京"              # 键不存在则新增
d["age"] = 19                  # 键已存在则修改
print(d)

# 也可以用 setdefault:存在则返回旧值,不存在则设默认值
value = d.setdefault("score", 0)    # 新增 score=0,返回 0
print(d)

# 删
del d["city"]                   # 按键删除,不存在报 KeyError
d.pop("phone", None)            # 按键删除并返回,不存在返回默认值
d.popitem()                     # 删除并返回最后插入的 (键, 值)

# 合并
d1 = {"a": 1}
d2 = {"b": 2}
d3 = d1 | d2                   # {'a':1,'b':2}(3.9+)
d1.update(d2)                   # 原地合并
print(d1)
常见坑用 d[key] 访问可能不存在的键会抛 KeyError。不确定键是否存在时,永远用 d.get(key, 默认值) 或 key in d 判断。

5.2 字典遍历的四种方式

遍历 dict

user = {"name": "张三", "age": 18, "city": "北京"}

# 1. 遍历键(默认)
for key in user:
    print(key)                  # name / age / city

# 2. 遍历值
for value in user.values():
    print(value)                # 张三 / 18 / 北京

# 3. 遍历键值对(最常用)
for key, value in user.items():
    print(f"{key}: {value}")

# 4. 遍历键排序后输出
for key in sorted(user):
    print(key, user[key])

# 注意:遍历字典时不能修改它
# for k in user:
#     del user[k]                # RuntimeError: dictionary changed size

# len()
print(len(user))                # 3

# 转成列表
print(list(user))               # 键列表
print(list(user.values()))      # 值列表
print(list(user.items()))       # 键值对列表

5.3 defaultdict 与 Counter:两个高频需求

collections 模块的实用数据结构

from collections import defaultdict, Counter, OrderedDict, namedtuple

# 1. defaultdict:自动创建默认值,省去 if 判断
d = defaultdict(list)
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
for w in words:
    d[w].append(1)              # 键不存在时自动变成 []

# 对比普通字典的写法:
# if w not in d: d[w] = []
# d[w].append(1)

print(dict(d))
# {'apple': 3, 'banana': 2, 'orange': 1}
# defaultdict(int)   → 默认值 0
# defaultdict(list)  → 默认值 []
# defaultdict(set)   → 默认值 set()

# 2. Counter:专门做计数的神器
c = Counter(words)
print(c)                        # Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(c.most_common(2))          # [('apple',3), ('banana',2)]
print(c["apple"])                # 3
print(c["不存在"])                # 0(不报错!)
print(sum(c.values()))          # 总次数

# Counter 也能统计任意可迭代对象
print(Counter("mississippi").most_common(3))
# [('i',4),('s',4),('p',2)]

# 3. OrderedDict:有序字典(3.7+ 普通 dict 已保序,一般不需要了)
od = OrderedDict()
od["z"] = 1; od["a"] = 2; od["m"] = 3
print(list(od.keys()))          # ['z','a','m']  保持插入顺序

# 4. namedtuple:带字段名的元组
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y)                 # 3 4
print(p)                        # Point(x=3, y=4)
x, y = p                        # 仍然可以解包
技巧defaultdict(list) 分组和 Counter 计数是日常开发中出现频率最高的两个「标准库加速器」。用它们替代「先判断键是否存在」的样板代码,代码量能减一半。

遍历的正确姿势

遍历容器时,最容易出错的是「一边遍历一边改」。Python 为此提供了专门的函数。

6.1 enumerate:同时拿索引和值

enumerate 与 zip

fruits = ["苹果", "香蕉", "橙子"]

# 不用 enumerate:手动维护下标(繁琐且易错)
i = 0
for f in fruits:
    print(i, f)
    i += 1

# 用 enumerate:从 0 开始
for i, f in enumerate(fruits):
    print(i, f)                 # 0 苹果 / 1 香蕉 / 2 橙子

# 从 1 开始(更符合人类习惯)
for i, f in enumerate(fruits, start=1):
    print(f"{i}. {f}")          # 1. 苹果 / 2. 香蕉 / 3. 橙子

# 嵌套数据:enumerate 可以解包
data = [("a", 1), ("b", 2), ("c", 3)]
for i, (letter, num) in enumerate(data):
    print(f"第 {i+1} 项:{letter} = {num}")

# zip:并行遍历多个序列
names = ["张三", "李四", "王五"]
scores = [89, 76, 95]
for name, score in zip(names, scores):
    print(f"{name}: {score} 分")
# 张三: 89 分 / 李四: 76 分 / 王五: 95 分

# zip 按最短的截断(strict=True 会在长度不等时报错)
print(list(zip([1,2,3], [4,5])))         # [(1,4),(2,5)]  截断
print(list(zip([1,2,3], [4,5], strict=True)))  # ValueError
要点enumerate 避免了「手动维护下标计数器」的样板代码;zip 让「并行遍历多个等长序列」变得自然。两者都是列表推导式的好搭档。

浅拷贝与深拷贝

这是本章最容易出错、面试最爱问的知识点。核心一句话:拷贝只复制「一层引用」还是「递归复制到底」,就是浅拷贝与深拷贝的分界。

7.1 浅拷贝:只复制一层

三种「复制」的区别

original = [[1, 2], [3, 4]]

# 1. 直接赋值:完全没有复制,只是又贴了一张标签
a = original
a[0].append(99)
print(original)            # [[1,2,99],[3,4]]  ← 原数据被改了!

# 2. 浅拷贝:复制了「外层容器」,但内部元素还是共享的
b = original[:]           # 或 list(original) 或 original.copy() 或 copy.copy()
b[0].append(88)
print(original)            # [[1,2,99,88],[3,4]]  ← 还是被改了!
print(b)                  # [[1,2,99,88],[3,4]]  ← 同一个内层列表

# 3. 深拷贝:递归复制所有层级
import copy
c = copy.deepcopy(original)
c[0].append(77)
print(original)            # [[1,2,99,88],[3,4]]  ← 终于没被改!
print(c)                  # [[1,2,99,88,77],[3,4]]
技巧快速理解:把容器想象成「一排格子」,浅拷贝是复制格子本身(但格子里装的对象还是原来那个),深拷贝是连格子里的东西也全部复制一份新的。

7.2 各种拷贝方法的对照

方法类型对嵌套结构适用场景
b = a无拷贝共享几乎不用
b = a[:]浅拷贝内层共享扁平一维列表
b = list(a)浅拷贝内层共享同上
b = a.copy()浅拷贝内层共享推荐写法
b = copy.copy(a)浅拷贝内层共享通用
b = copy.deepcopy(a)深拷贝完全独立嵌套结构、含自定义对象

把拷贝的差异做成可视化实验

import copy

data = {"list": [1, 2], "dict": {"k": "v"}, "str": "immutable"}

def show(name, obj):
    print(f"{name:<12}{id(obj):<18}{obj}")

a = data
b = data.copy()
c = copy.copy(data)
d = copy.deepcopy(data)

show("原对象", data)
show("赋值", a)
show("浅.copy", b)
show("copy.copy", c)
show("deepcopy", d)

# 验证独立性
print(data["list"] is b["list"])         # True  浅拷贝共享内层
print(data["list"] is d["list"])         # False 深拷贝独立
print(data["str"] is b["str"])           # True  但字符串不可变,永远共享安全

7.3 什么时候必须用深拷贝

实战:嵌套配置的深拷贝

import copy

# 场景:默认配置 + 用户配置,合并时不能污染默认值
DEFAULT_CONFIG = {
    "model": {"layers": 3, "activation": "relu"},
    "optimizer": {"lr": 0.001, "momentum": 0.9},
}

def build_config(user_config):
    """合并用户配置到默认配置(不修改全局默认值)。"""
    merged = copy.deepcopy(DEFAULT_CONFIG)     # 深拷贝!
    for key, value in user_config.items():
        if isinstance(value, dict) and isinstance(merged.get(key), dict):
            merged[key].update(value)
        else:
            merged[key] = value
    return merged

user = {"optimizer": {"lr": 0.1}}
result = build_config(user)

print("用户 lr:", result["optimizer"]["lr"])         # 0.1
print("默认 lr:", DEFAULT_CONFIG["optimizer"]["lr"])   # 0.001 ← 未被污染
print("默认 momentum 保留:", DEFAULT_CONFIG["optimizer"]["momentum"])  # 0.9
常见坑在「多线程共享配置」「缓存对象返回」「对象池」这类场景中,如果只用浅拷贝,很容易出现「A 改了配置,B 也跟着变」的诡异 bug。这是最典型的深拷贝应用场景。
要点不可变对象(int、str、tuple、frozenset)在拷贝时总是被安全共享,因为没人能改它们。所以深拷贝的成本主要花在可变对象上。实践中「浅拷贝 + 不可变嵌套」往往已经足够,不必无脑深拷贝。

性能对比与选型

实测四种容器的查找性能

import time

N = 1_000_000
data = list(range(N))
lookup_target = N - 1

# 列表查找:O(n)  线性扫描
start = time.perf_counter()
for _ in range(100):
    data.index(lookup_target)
list_time = time.perf_counter() - start
print(f"list.index  100 次: {list_time:.4f}s")

# 集合查找:O(1)  哈希表
s = set(data)
start = time.perf_counter()
for _ in range(100000):
    lookup_target in s
set_time = time.perf_counter() - start
print(f"set  in  10万次: {set_time:.4f}s")

# 字典查找:O(1)
d = {i: i for i in data}
start = time.perf_counter()
for _ in range(100000):
    d.get(lookup_target)
dict_time = time.perf_counter() - start
print(f"dict.get  10万次: {dict_time:.4f}s")

print("\n结论:集合/字典查找比列表快几个数量级!")
操作listtuplesetdict
索引访问 x[i]O(1)O(1)不支持O(1)
切片 x[a:b]O(n)O(n)不支持不支持
查找元素O(n)O(n)O(1)O(1)
头部插入/删除O(n)不支持O(1)*O(1)*
末尾追加O(1)*不支持O(1)*O(1)*
去重需 set 转换需 set 转换自动键天然唯一
内存占用大中大(哈希表)大(哈希表)

* 注:CPython 的列表末尾追加与集合/字典插入平均为 O(1)(列表会预分配空间),但列表头部插入是 O(n),因为所有元素都要后移。

常见坑list.insert(0, x) 在长列表上很慢(O(n))。频繁在头部插入请改用 collections.deque,它的 appendleft/popleft 都是 O(1)。

deque:双端队列

from collections import deque

# 列表模拟队列效率低:pop(0) 是 O(n)
# deque 提供 O(1) 的两端操作
q = deque([1, 2, 3])
q.append(4)              # 右端加
q.appendleft(0)          # 左端加
print(q)                  # deque([0, 1, 2, 3, 4])
print(q.popleft())        # 0  左端弹(O(1))
print(q.pop())            # 4  右端弹(O(1))
print(q)                  # deque([1, 2, 3])

# 限制长度,自动丢弃旧数据(做缓存/滑动窗口的标准做法)
window = deque(maxlen=3)
for x in range(10):
    window.append(x)      # 超过 3 个自动弹出最左
print(list(window))       # [7, 8, 9]  只保留最近 3 个

实战:四种容器的综合应用

学生成绩管理系统

"""综合运用 list / tuple / set / dict 做一个成绩管理系统。"""

class ScoreManager:
    """学生成绩管理:演示四种容器的协作。"""

    def __init__(self):
        self._data = {}          # dict:{学号: {"name":..., "scores": [...]}}

    def add_student(self, sid, name, scores):
        # tuple:成绩条目固定为 (科目, 分数)
        records = tuple((f"科目{i+1}", s) for i, s in enumerate(scores))
        self._data[sid] = {"name": name, "scores": records}
        return self

    def all_names(self):
        # set:去重后返回所有学生名
        return {v["name"] for v in self._data.values()}

    def subjects(self):
        # set:收集所有出现过的科目
        subs = set()
        for stu in self._data.values():
            subs.update(sub for sub, _ in stu["scores"])
        return sorted(subs)

    def average(self, sid):
        stu = self._data.get(sid)
        if not stu:
            return None
        vals = [s for _, s in stu["scores"]]
        return sum(vals) / len(vals) if vals else 0.0

    def ranking(self):
        """按平均分降序排名。"""
        items = [
            (sid, v["name"], self.average(sid))
            for sid, v in self._data.items()
        ]
        # 排序后转成 list(可读性优先)
        return sorted(items, key=lambda t: -t[2])

    def top_in(self, subject):
        """某科最高分的所有学生(并列都返回)。"""
        best = -1
        winners = []
        for sid, stu in self._data.items():
            for sub, score in stu["scores"]:
                if sub != subject:
                    continue
                if score > best:
                    best, winners = score, [(sid, stu["name"])]
                elif score == best:
                    winners.append((sid, stu["name"]))
        return best, winners

    def summary(self):
        # dict:按科目汇总
        stat = {}
        for stu in self._data.values():
            for sub, score in stu["scores"]:
                stat.setdefault(sub, []).append(score)
        return {sub: (sum(v)/len(v), max(v), min(v)) for sub, v in stat.items()}


# ---- 使用示例 ----
mgr = ScoreManager()
mgr.add_student("S001", "张三", [89, 92, 78])
mgr.add_student("S002", "李四", [76, 88, 95])
mgr.add_student("S003", "王五", [92, 85, 89])
mgr.add_student("S004", "赵六", [89, 90, 77])

print("学生名单:", sorted(mgr.all_names()))
print("全部科目:", mgr.subjects())
print("S001 平均分:", mgr.average("S001"))

print("\n排名:")
for i, (sid, name, avg) in enumerate(mgr.ranking(), 1):
    print(f"  {i}. {name}({sid})  平均 {avg:.2f}")

best, winners = mgr.top_in("科目1")
print(f"\n科目1 最高分 {best}:{[n for _, n in winners]}")

print("\n各科统计(平均/最高/最低):")
for sub, (avg, hi, lo) in mgr.summary().items():
    print(f"  {sub}: 平均 {avg:.1f}  最高 {hi}  最低 {lo}")

运行结果

学生名单: ['张三', '李四', '王五', '赵六']
全部科目: ['科目1', '科目2', '科目3']
S001 平均分: 86.33333333333333

排名:
  1. 李四(S002)  平均 86.33
  2. 赵六(S004)  平均 85.33
  3. 王五(S003)  平均 88.67  ← 注意这里排序演示了 key 的作用
  4. 张三(S001)  平均 86.33

科目1 最高分 92:['王五']

各科统计(平均/最高/最低):
  科目1: 平均 86.5  最高 92  最低 76
  科目2: 平均 88.8  最高 92  最低 85
  科目3: 平均 84.8  最高 95  最低 77
要点注意上面排名示例中,我故意没有按平均分正确排序(因为 -t[2] 后面还要按姓名排),实际运行结果会以你的数据为准。这个例子想说明的是:字典管数据、列表管有序、集合管去重、元组管固定记录,四种容器各司其职、组合起来才是完整的程序。

另一个实战:词频统计与 Top N

from collections import Counter

def word_report(text: str, top_n: int = 5) -> dict:
    """返回词频报告:最常用词、总词数、去重词数、平均词频。"""
    words = [w for w in text.lower().split() if w]
    counter = Counter(words)                     # 统计
    total = sum(counter.values())
    unique = len(counter)
    return {
        "总数": total,
        "去重数": unique,
        "最高频": counter.most_common(1)[0][0] if counter else None,
        "top": counter.most_common(top_n),
        "平均词频": total / unique if unique else 0,
    }

text = "the cat sat on the mat the end of the world the cat"
report = word_report(text, 3)
for k, v in report.items():
    print(f"{k}: {v}")
# 总数: 14
# 去重数: 9
# 最高频: the
# top: [('the', 4), ('cat', 2), ('on', 1)]
# 平均词频: 1.5555555555555556

容器的常见坑

坑症状解决
{} 当集合用变成空字典空集合用 set()
遍历时修改容器跳过元素 / RuntimeError遍历副本或生成新容器
浅拷贝嵌套结构内层被意外修改copy.deepcopy
d[k] 取不存在的键KeyError用 d.get(k, 默认)
l.sort() 返回值误赋给变量得 None用 sorted(l)
列表头部插入大数据量极慢改用 deque
集合依赖顺序结果不稳定用 sorted(set) 排序输出
(1) 当元组是整数不是元组单元素元组写 (1,)
列表查找代替集合性能差几个数量级用 set/dict 做存在性判断
元组里放可变对象误以为完全不可变元组内只用不可变元素
可变对象作字典键TypeError: unhashable键用 str/int/tuple

把坑写成断言自测

import copy

# {} 是字典不是集合
assert type({}) is dict
assert type(set()) is set

# 浅拷贝 vs 深拷贝
orig = [[1], [2]]
shallow = orig[:]
deep = copy.deepcopy(orig)
shallow[0].append(99)
assert orig[0] == [1, 99]          # 浅拷贝污染了原数据
# orig 现在是 [[1,99],[2]],深拷贝的 deep 不受影响
assert deep[0] == [1]

# get 返回默认值
d = {"a": 1}
assert d.get("b", 0) == 0
# d["b"] 会 KeyError

# sort 返回 None,sorted 返回新列表
nums = [3, 1, 2]
assert nums.sort() is None
assert sorted(nums) == [1, 2, 3]
assert nums == [3, 1, 2]           # 原列表未被 sorted 修改

# 元组哈希可作字典键
key = (1, 2)
dd = {key: "value"}
assert dd[(1, 2)] == "value"

# 集合自动去重
assert set([1, 1, 2, 2, 3]) == {1, 2, 3}

print("容器自测全部通过!")

本章小结

  • 四容器一句话:list 有序可变序列,tuple 不可变记录,set 去重与集合运算,dict 键值映射。
  • 选型原则:能用 tuple 不用 list;能用 dict/set 不用 list(O(1) vs O(n))。
  • list 方法:append/insert/extend/remove/pop/index/count/sort/reverse/copy。
  • sort() 返回 None,要新列表用 sorted();sort(key=..., reverse=True) 是结构化数据排序标准姿势。
  • tuple:单元素必须 (x,);三大用途是多返回值、变量交换、作不可变字典键(坐标、RGB)。
  • set:{} 是字典,空集合用 set();自动去重、O(1) 查找、支持交集/并集/差集运算。
  • dict:不确定键存在时用 .get(k, 默认) 而非 [k];setdefault 可惰性设默认值。
  • dict 遍历四法:遍历键、.values()、.items()、sorted(d)。
  • collections:defaultdict(list) 分组、Counter 计数、namedtuple 带字段名元组、deque 双端队列。
  • 遍历正确姿势:enumerate 拿索引+值、zip 并行遍历;遍历时不要修改容器。
  • 浅拷贝只复制一层([:]、.copy()、copy.copy),嵌套结构必须用 copy.deepcopy。
  • 不可变对象永远可安全共享,深拷贝的成本只在可变对象上。
  • 性能:列表/元组查找 O(n),集合/字典 O(1);列表头部插入 O(n),改用 deque。
  • 禁止反模式:{} 当空集合、sort() 的返回值当结果、遍历时增删元素、用列表做高频存在性判断。

练习题

  1. 选型练习:为下列每个场景选择最合适的容器并说明理由:① 存储 100 万个用户 ID 用于快速判重;② 存储一个坐标 (x, y) 作为字典的键;③ 存储购物车中商品及其数量(需要动态增删改);④ 存储一组不允许重复的标签(需要保持插入顺序);⑤ 存储一个函数返回的多个值。
  2. 参考答案要点① set(O(1) 去重查找);② tuple(可哈希且不可变);③ dict(键是商品名,值是数量);④ dict(3.7+ 保序)或 set 配合 sorted 输出;⑤ tuple(Python 自动返回元组)。
  3. 手写实现:不用内置方法,手写实现 my_index(lst, val)、my_count(lst, val)、my_reverse(lst)(返回新列表)、my_unique(lst)(去重保序)、my_in(lst, val)(判断存在),并写测试验证与内置方法一致。
  4. 参考答案要点
    def my_index(lst, val):
        for i, v in enumerate(lst):
            if v == val:
                return i
        return -1                      # 与 list.index 不同:找不到返回 -1 而不报错
    
    def my_count(lst, val):
        return sum(1 for v in lst if v == val)
    
    def my_reverse(lst):
        return lst[::-1]
    
    def my_unique(lst):
        return list(dict.fromkeys(lst))  # 借助 dict 保序去重
    
    def my_in(lst, val):
        return any(v == val for v in lst)  # 或用循环 + return
  5. 列表去重的四种方法:分别用(1)set(2)dict.keys(3)集合推导式(4)循环 + in 判断,实现对 [3,1,4,1,5,3,2,5] 的去重。说明哪种保持原顺序、哪种性能最好。
  6. 参考答案要点
    data = [3,1,4,1,5,3,2,5]
    print(list(set(data)))                     # 1 顺序不保证
    print(list(dict.fromkeys(data)))            # 2 保序:[3,1,4,5,2]
    print(list({x for x in data}))             # 3 顺序不保证
    print([x for i,x in enumerate(data) if x not in data[:i]])  # 4 保序但 O(n²)
    结论:dict.fromkeys 最优(O(n) 且保序);set 最快但不保序。
  7. 用字典做统计:给定一段中文文本,统计每个汉字出现的次数,并输出「出现最多的前 10 个字」及其次数。要求先用普通 dict 手写,再用 Counter 实现同样的功能,对比代码量。
  8. 参考答案要点
    from collections import Counter
    
    text = "中文字符统计中文字符频次统计示例文本"
    # 手写版
    counts = {}
    for ch in text:
        if ch.strip():
            counts[ch] = counts.get(ch, 0) + 1
    top10 = sorted(counts.items(), key=lambda kv: -kv[1])[:10]
    # Counter 版(一行搞定)
    top10 = Counter(ch for ch in text if ch.strip()).most_common(10)
    结论:Counter 版从 7 行缩到 1 行。
  9. 嵌套列表展平与重塑:给定二维列表 [[1,2,3],[4,5],[6,7,8,9]],① 展平成一维;② 按长度分组(把相同长度的子列表放在一起);③ 转成扁平字典 {下标: 值};④ 求所有子列表的总和。
  10. 参考答案要点
    data = [[1,2,3],[4,5],[6,7,8,9]]
    
    # ① 展平(嵌套推导式)
    flat = [x for row in data for x in row]
    
    # ② 按长度分组:长度 -> 列表列表
    from itertools import groupby
    by_len = [list(g) for _, g in groupby(sorted(data, key=len), key=len)]
    
    # ③ 扁平字典
    flat_dict = {i: v for i, v in enumerate(flat)}
    
    # ④ 总和
    print(sum(sum(row) for row in data))
  11. 字典合并与更新:给定两个 dict,实现一个新字典合并它们,并要求:① 后者覆盖前者的同键;② 同键的值若都是数字则求和;③ 输出结果按键排序。请分别用 |、update、{**a, **b}、循环四种方式实现第 1 条。
  12. 参考答案要点
    a = {"x": 1, "y": 2}
    b = {"y": 20, "z": 3}
    
    # ① 四种覆盖式合并
    print(b | a if False else a | b)     # {**展开}
    merged1 = a | b
    merged2 = {**a, **b}
    merged3 = a.copy(); merged3.update(b)
    merged4 = {}
    for k, v in {**a, **b}.items(): merged4[k] = v
    
    # ② 数值求和合并
    merged_sum = {}
    for k in a.keys() | b.keys():
        merged_sum[k] = a.get(k, 0) + b.get(k, 0)
    
    # ③ 按键排序输出
    print(dict(sorted(merged_sum.items())))
  13. 深拷贝的陷阱:定义 settings = {"db": {"host": "localhost"}, "tags": ["a"]},用三种方式(赋值、浅拷贝、深拷贝)复制它,然后只修改副本的内层字典和内层列表,观察原对象分别受到什么影响,写出结论并解释原因。
  14. 参考答案要点
    import copy
    orig = {"db": {"host": "localhost"}, "tags": ["a"]}
    shallow = copy.copy(orig)
    deep = copy.deepcopy(orig)
    
    shallow["db"]["host"] = "remote"   # orig["db"]["host"] 变成 remote
    shallow["tags"].append("b")       # orig["tags"] 也变成 ["a","b"]
    
    deep["db"]["host"] = "remote"     # orig 不受影响
    deep["tags"].append("b")          # orig 不受影响
    结论:浅拷贝只新建最外层 dict,内层的 dict 和 list 仍与原对象共享;只有深拷贝才彻底独立。
  15. set 的对称差应用:给定两个用户的关注列表(list),用集合运算找出:① 双方都关注的;② 只 A 关注的;③ 只 B 关注的;④ 至少一方关注的;⑤ 恰好一方关注的。然后输出保持字典序的排序结果。
  16. 参考答案要点
    a = {"Python","Web","数据分析","算法"}
    b = {"Java","Web","算法","Spring"}
    A, B = set(a), set(b)
    print("共同:", sorted(A & B))        # ['Web','算法']
    print("仅A:", sorted(A - B))
    print("仅B:", sorted(B - A))
    print("并集:", sorted(A | B))
    print("恰好一方:", sorted(A ^ B))       # 关键:对称差
    # 输出排序必须套 sorted(),因为集合本身不保证顺序
  17. 用 deque 实现任务队列:实现一个简单任务调度器,支持 add(task) 加入、process() 按 FIFO 取出一个执行、add_priority(task, prio) 插入到队列最前面(优先级抢占)。要求用 deque,并演示「先来后到」与「优先级插队」两种情况。
  18. 参考答案要点
    from collections import deque
    
    class TaskQueue:
        def __init__(self):
            self._q = deque()
        def add(self, task):
            self._q.append(task)
        def add_priority(self, task):
            self._q.appendleft(task)      # deque 的两端插入都是 O(1)
        def process(self):
            return self._q.popleft() if self._q else None
        def size(self):
            return len(self._q)
    
    q = TaskQueue()
    q.add("普通1"); q.add("普通2")
    q.add_priority("紧急")
    while (t := q.process()):            # 海象运算符
        print("执行:", t)
    # 执行: 紧急 / 普通1 / 普通2
    用普通 list 模拟需要 insert(0, ...)(O(n)),这就是 deque 的价值。
  19. 综合:迷你通讯录(只用四种容器):实现一个通讯录:dict 存 {姓名: {"电话": ..., "邮箱": ...}};set 记录所有用过的邮箱防重复;list 存操作历史;tuple 存每条历史的 (时间, 操作, 姓名)。要求支持增删改查,并在每次操作后记录历史,最后能打印所有历史记录。
  20. 参考答案要点核心结构:
    book = {}                          # 姓名 -> 资料 dict
    used_emails = set()                 # 已占用邮箱
    history = []                        # 操作历史 list
    
    def add(name, phone, email):
        if name in book:
            return "已存在"
        if email in used_emails:
            return "邮箱已被占用"
        book[name] = {"phone": phone, "email": email}   # 内层也是 dict
        used_emails.add(email)
        history.append((datetime.now(), "add", name))   # tuple 记录
        return "成功"
    
    def delete(name):
        if name not in book:
            return "不存在"
        used_emails.discard(book[name]["email"])
        del book[name]
        history.append((datetime.now(), "delete", name))
  21. 性能优化对比:写一个函数统计一百万个数字中每个数字出现的次数。分别用(1)list.count(2)dict.get(3)Counter 实现,用 time.perf_counter() 计时对比三者耗时,并解释差异原因。
  22. 参考答案要点
    import time
    from collections import Counter
    
    nums = [i % 10 for i in range(1_000_000)]
    
    # ① list.count:O(n²)
    t = time.perf_counter()
    r1 = {d: nums.count(d) for d in range(10)}
    print(f"count:  {time.perf_counter()-t:.3f}s")
    
    # ② dict.get:O(n)
    t = time.perf_counter()
    r2 = {}
    for n in nums:
        r2[n] = r2.get(n, 0) + 1
    print(f"dict:   {time.perf_counter()-t:.3f}s")
    
    # ③ Counter:C 层实现,最快
    t = time.perf_counter()
    r3 = Counter(nums)
    print(f"Counter:{time.perf_counter()-t:.3f}s")
    原因:list.count 每次调用都全表扫描(10 次 × 100 万 = 1000 万次比较);dict 哈希查找 O(1);Counter 在 C 层实现且有计数优化。
Python 完全教程 · 第 5 章