Course Builder个人学习工作台 · 本地模式
Modern Python:面向有经验开发者的回归路线

容器、切片、解包与推导式

目标:能够根据顺序、唯一性、查找方式和可变性选择 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}

注意三个决定:

  1. {**item, "text": text} 创建新字典,右侧同名键覆盖旧值,原输入没有被原地修改。
  2. dict.fromkeys(...) 利用字典插入顺序去重,保留标签第一次出现的顺序;直接 set(tags) 不能提供这个展示契约。
  3. .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() 不总比 [] 安全:缺失本应是错误时,[] 更快暴露问题。
  • 推导式中调用有副作用的函数:改回循环,让执行顺序和错误位置可见。

参考资料