容器、切片、解包与推导式
目标:能够根据顺序、唯一性、查找方式和可变性选择 list、tuple、set 或 dict,并用可读的推导式完成一次数据转换。
Python 内建容器不是四种拼写不同的“数组”。它们各自表达不同契约:
| 类型 | 顺序 | 可变 | 重复 | 典型用途 |
|---|---|---|---|---|
list[T] | 有 | 是 | 允许 | 按顺序收集同类元素 |
tuple[...] | 有 | 否(元素自身仍可变) | 允许 | 固定结构、一次返回多个值 |
set[T] | 不应依赖展示顺序 | 是 | 不允许 | 去重、成员测试、集合运算 |
dict[K, V] | 保持插入顺序 | 是 | 键唯一 | 按键查找、保存记录字段 |
选择容器时先问:结果是否需要顺序?是否允许重复?调用方是否要修改?查找是按位置还是按键?
完整示例:整理批量消息
保存为 containers_lab.py:
from __future__ import annotations
def normalize_messages(raw: list[dict[str, object]]) -> list[dict[str, object]]:
normalized: list[dict[str, object]] = []
for item in raw:
text = str(item.get("text", "")).strip()
tags = item.get("tags", [])
if not isinstance(tags, list):
raise TypeError("tags must be a list")
clean_tags = list(dict.fromkeys(str(tag).lower() for tag in tags))
if text:
normalized.append({**item, "text": text, "tags": clean_tags})
return normalized
def tag_counts(messages: list[dict[str, object]]) -> dict[str, int]:
counts: dict[str, int] = {}
for message in messages:
for tag in message["tags"]:
key = str(tag)
counts[key] = counts.get(key, 0) + 1
return counts
if __name__ == "__main__":
source = [
{"text": " 需要退款 ", "tags": ["VIP", "refund", "VIP"]},
{"text": "", "tags": ["ignored"]},
{"text": "查询进度", "tags": ["status"]},
]
messages = normalize_messages(source)
print(messages)
print(tag_counts(messages))
assert source[0]["text"] == " 需要退款 "
运行:
uv run python containers_lab.py
预期输出:
[{'text': '需要退款', 'tags': ['vip', 'refund']}, {'text': '查询进度', 'tags': ['status']}]
{'vip': 1, 'refund': 1, 'status': 1}
注意三个决定:
{**item, "text": text}创建新字典,右侧同名键覆盖旧值,原输入没有被原地修改。dict.fromkeys(...)利用字典插入顺序去重,保留标签第一次出现的顺序;直接set(tags)不能提供这个展示契约。.get("tags", [])只处理缺失键,不能证明已有值真的是 list,所以仍有显式检查。
切片和解包
items[start:stop:step] 创建一个新 list(对普通 list 而言是浅拷贝)。负索引从末尾计数。扩展解包可以把头尾与中间部分分开:
first, *middle, last = [10, 20, 30, 40]
# first == 10, middle == [20, 30], last == 40
当右侧元素不足时会抛 ValueError,所以解包也在表达结构约束。
引导实验:何时使用推导式
把 normalize_messages 中的标签处理拆开:
lowered = [str(tag).lower() for tag in tags]
clean_tags = list(dict.fromkeys(lowered))
这比把过滤、去重、转换和副作用塞进一个多层推导式更容易检查。推导式适合“从一个 iterable 映射/过滤成一个新容器”;一旦逻辑需要多步状态、异常处理或日志,普通循环通常更清楚。
独立练习
新增函数 recent_texts(messages, limit=2),返回最后 limit 条文本,不修改输入;limit <= 0 返回空 list。
参考实现与验收
def recent_texts(messages: list[dict[str, object]], limit: int = 2) -> list[str]:
if limit <= 0:
return []
return [str(item["text"]) for item in messages[-limit:]]
assert recent_texts([{"text": "a"}, {"text": "b"}, {"text": "c"}]) == ["b", "c"]
assert recent_texts([{"text": "a"}], 0) == []
常见误判
tuple并不深度不可变:([1],)中的 list 仍能修改。dict.get()不总比[]安全:缺失本应是错误时,[]更快暴露问题。- 推导式中调用有副作用的函数:改回循环,让执行顺序和错误位置可见。