哈希表、字典、集合与计数

本节目标

使用字典表达键值映射、集合表达唯一成员、Counter 表达频次,并理解哈希、可哈希键、顺序、缺失值与幂等去重的边界。

字典:从键找到值

tools = {
    "search": {"read_only": True},
    "send_email": {"read_only": False},
}
 
print(tools["search"])
print(tools.get("unknown"))  # 不存在时返回 None

字典键必须唯一。tools["unknown"] 会抛出 KeyError;get 适合“缺失是正常分支”,但若 None 也是合法值,应使用成员测试区分:

if "unknown" not in tools:
    print("tool is not registered")

不要用 dict.get(key, expensive_call()) 期待只有缺失时才调用:函数参数会先求值。需要时写明确的 if。

Python 字典保持插入顺序,但这不等于按键排序。删除后重新插入通常进入末尾;若接口要求按键、时间或优先级输出,应显式 sorted(...) 并测试。集合不承诺可依赖的迭代顺序,确定性输出前要排序或保留独立顺序结构。

集合:只关心成员是否出现

seen_call_ids = set()
 
call_id = "call-42"
if call_id in seen_call_ids:
    raise ValueError("duplicate call")
seen_call_ids.add(call_id)

集合自动消除重复,支持并集 |、交集 &、差集 -。它不保存重复次数;要计数可用字典或 collections.Counter。

Counter 与 defaultdict

from collections import Counter, defaultdict
 
errors = Counter(["timeout", "rate_limit", "timeout"])
print(errors["timeout"])  # 2
print(errors["missing"])  # 0,不抛 KeyError
 
by_tool: defaultdict[str, list[str]] = defaultdict(list)
by_tool["search"].append("run-1")

Counter 是计数语义,不只是“默认值为 0 的字典”;负数/零计数、集合式运算等行为应查文档。defaultdict 只在 __getitem__ 缺键时调用 default_factory;mapping.get(key) 不会触发工厂。不要让一次看似只读查询意外创建大量键。

哈希的最小直觉

字典和集合用哈希值帮助定位槽位。键需要在作为键期间保持可哈希语义,因此字符串、整数以及只含可哈希元素的元组通常可作键,列表和字典不可作键。

cache = {}
cache[("model-a", "prompt-v1")] = "result"
 
mutable_key = ["model-a", "prompt-v1"]
# cache[mutable_key] = "result"  # TypeError

不同键可能产生哈希冲突,运行时会进一步比较。平均 查找不是密码学安全保证;不要把内置 hash() 当稳定持久 ID,因为它不承诺跨进程稳定。

嵌套状态与安全更新

runs = {
    "run-1": {"status": "queued", "attempt": 0},
}
 
run = runs["run-1"]
run["attempt"] += 1
run["status"] = "running"

run 与 runs["run-1"] 指向同一内部字典。更新方便,但并发环境需要锁、事务或外部状态存储,不能假设“字典单行操作”就构成完整业务原子性。

去重与幂等不是同一件事

  • 去重:识别同一标识是否已出现。
  • 幂等:同一业务请求重复执行,不产生额外副作用。

内存集合能在当前进程发现重复 call_id,进程重启后会丢失;多实例之间也不共享。发送消息、扣费等写操作需要持久化幂等键,并把“检查和写入结果”放进能保证一致性的存储操作。

哈希与相等必须一致

若两个对象按 == 相等,它们必须产生相同哈希才能安全用作键。自定义可变对象若参与相等比较,通常不应在入表后改变影响相等/哈希的字段。内置 hash() 也不承诺跨进程稳定,不能代替内容摘要、数据库主键或幂等键。

练习

  1. 把工具列表转换成 tool_name → 是否只读 的字典。
  2. 给定两个模型支持的能力集合,求共同能力和只在第一个模型中的能力。
  3. 使用 Counter 统计一批错误码出现次数。
  4. 设计一个幂等记录,至少包含请求键、状态、结果引用和创建时间。
  5. 验证 defaultdict.get("missing") 不会创建键,而 mapping["missing"] 会;解释为什么。
  6. 把一个集合直接转 JSON 输出前,设计确定性排序与不可比较元素策略。

自测

  1. 为什么列表不能作为字典键?
  2. mapping[key] 与 mapping.get(key) 如何选择?
  3. 集合去重会丢失什么信息?
  4. 为什么内存集合不足以保证生产写操作幂等?
  5. 字典保持插入顺序是否表示键已排序?
  6. defaultdict.get() 会调用 default_factory 吗?

下一步与相关概念

下一节:堆与稳定优先队列。

参考资料

获取日期:2026-07-14。