前言
前面谈字典的文章多半围绕「语法」——怎么建、怎么读、有什么方法。但真实项目里,字典最常出现的地方是它解决的具体问题:读配置、数次数、把一堆记录按某个字段分堆、缓存重复计算的结果、和 JSON 互相转换。这篇换一个角度,专讲实战用法。
选这个角度是有原因的:字典的 API 其实很少,真正让人卡住的往往不是「不会用get」,而是「不知道该用字典」或者「用了却埋了个坑」。比如 JSON 的键只能是字符串,Python 字典的键却可以是元组,直接json.dumps就会报错;再比如用setdefault分组比分堆写法省事,但写错了会往字典里塞进空桶。
本文给出五个典型场景,每个都是可复制运行的完整片段,以 Python 3.8 为基准,涉及 3.9 的特性会标出版本。最后一节列出实战中最容易踩的坑。
一、配置管理:用字典承载一层默认配置
配置最常见的需求是「默认值 + 用户覆盖」。字典的合并正好干这个。
# 适用于 Python 3.8+
DEFAULTS = {
"host": "127.0.0.1",
"port": 8000,
"debug": False,
"timeout": 30,
}
def load_config(user_config):
# 3.8 及以前:用 {**a, **b} 造新字典,不改动默认值
merged = {**DEFAULTS, **user_config}
return merged
cfg = load_config({"port": 9000, "debug": True})
print(cfg["port"]) # 9000,用户值覆盖默认
print(DEFAULTS["port"]) # 8000,默认值没被改如果你在Python 3.9+,可以写得更直观:merged = DEFAULTS | user_config。注意{**a, **b}与a | b都返回新字典,不修改原来的DEFAULTS,这在配置场景里很重要——默认值一旦被污染,后续调用全乱。
访问配置项时优先用get给默认值,避免缺键崩溃:
# 适用于 Python 3.8+
print(cfg.get("retries", 3)) # 3,没配就用兜底二、计数:统计出现次数
统计词频、投票、日志里各状态出现次数,都是同一个套路。
# 适用于 Python 3.8+
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1 # 缺键当 0 再加一
print(counts) # {'apple': 3, 'banana': 2, 'cherry': 1}同样的活,标准库的collections.Counter一行就能干,而且缺键自动返回0:
# 适用于 Python 3.8+
from collections import Counter
counts = Counter(words)
print(counts.most_common(2)) # [('apple', 3), ('banana', 2)]一般规则:临时数一数用get也行;要排序取前几名、要做计数之间的加减,Counter更合适。
三、分组:用setdefault建桶
把一列「(类别, 元素)」按类别归堆,是字典最实用的用法之一。
# 适用于 Python 3.8+
records = [
("水果", "苹果"),
("蔬菜", "胡萝卜"),
("水果", "香蕉"),
("蔬菜", "土豆"),
("水果", "苹果"),
]
buckets = {}
for category, item in records:
buckets.setdefault(category, []).append(item)
print(buckets)
# {'水果': ['苹果', '香蕉', '苹果'], '蔬菜': ['胡萝卜', '土豆']}这行的关键在setdefault(category, []):键不在,就先把[]放进去并返回它;键在,直接返回已有的列表。列表是原地append,所以改动会体现在字典里。
如果连setdefault都不想写,可以用collections.defaultdict(list),它会在缺键时自动调用list()建桶:
# 适用于 Python 3.8+
from collections import defaultdict
buckets = defaultdict(list)
for category, item in records:
buckets[category].append(item)要注意defaultdict的副作用:即使只是「读」一个不存在的键,也会把它建出来。
四、简易缓存:记忆化
同一个参数反复计算,可以把结果存进字典复用。手写版本如下:
# 适用于 Python 3.8+
cache = {}
def slow_square(n):
if n in cache:
return cache[n]
result = n * n # 这里假装是很贵的计算
cache[n] = result
return result
print(slow_square(4)) # 16
print(slow_square(4)) # 16,第二次直接命中缓存键必须可哈希,所以参数是列表时不能直接做键,得先转成元组:
# 适用于 Python 3.8+
def key_of(seq):
return tuple(seq) # 列表 → 元组,才能当键手写缓存只是演示原理。生产代码里更该用标准库的functools.lru_cache:
# 适用于 Python 3.8+
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(10)) # 55五、和 JSON 互转:注意键只能是字符串
字典和 JSON 对象长得几乎一样,json模块能直接互转。
# 适用于 Python 3.8+
import json
data = {"name": "小明", "age": 18, "tags": ["a", "b"]}
text = json.dumps(data, ensure_ascii=False)
print(text) # {"name": "小明", "age": 18, "tags": ["a", "b"]}
back = json.loads(text)
print(back["name"]) # 小明这里有个必须知道的区别:Python 字典的键可以是元组、整数、浮点,而 JSON 对象的键只能是字符串。序列化时非字符串键会被转换,反序列化回来就变不回原类型了。
# 适用于 Python 3.8+
import json
d = {1: "one", (2, 3): "pair"}
print(json.dumps(d)) # {"1": "one", "[2, 3]": "pair"}
# 键被转成字符串:'1' 和 '[2, 3]'所以「字典 → JSON → 字典」不保证能还原。如果键是元组或整数,往返一趟就变了。
常见坑点
坑一:合并配置时改坏了默认值。
# 适用于 Python 3.8+
# ❌ 把用户配置直接 update 进默认值,默认值被永久污染
DEFAULTS = {"port": 8000}
DEFAULTS.update({"port": 9000}) # DEFAULTS 变成 {'port': 9000}
# ✅ 用原默认值造新字典,不动原对象
BASE = {"port": 8000}
merged = {**BASE, "port": 9000}
print(BASE) # {'port': 8000}
print(merged) # {'port': 9000}坑二:计数时忘了给默认值。
# 适用于 Python 3.8+
# ❌ 直接累加会 KeyError
counts = {}
counts["apple"] += 1 # KeyError: 'apple'
# ✅ 用 get 或 defaultdict
counts["apple"] = counts.get("apple", 0) + 1坑三:用get建桶,数据悄悄丢了。
# 适用于 Python 3.8+
records = [("水果", "苹果"), ("蔬菜", "胡萝卜")]
# ❌ get 只在缺键时返回一个临时列表,append 后没人保存,数据丢了
buckets = {}
for k, v in records:
buckets.get(k, []).append(v) # 临时列表用完即弃
print(buckets) # {},什么都没存进去
# ✅ 用 setdefault,它会把新桶写回字典
for k, v in records:
buckets.setdefault(k, []).append(v)
print(buckets) # {'水果': ['苹果'], '蔬菜': ['胡萝卜']}坑四:把列表塞进 JSON 的键。
# 适用于 Python 3.8+
# ❌ 列表不能当字典键,也就无法参与序列化
d = {["a"]: 1} # TypeError(构造时就失败)
# ✅ 键用字符串;需要复合键先自己拼好
d = {"a,b": 1}坑五:以为 JSON 往返能还原键的类型。
# 适用于 Python 3.8+
# ❌ 整数键往返后会变字符串
import json
d = {1: "one"}
back = json.loads(json.dumps(d))
print(back) # {'1': 'one'}
# ✅ 需要整数键就自己转回去
back = {int(k): v for k, v in back.items()}坑六:自己手写的缓存没有上限。
# 适用于 Python 3.8+
# ❌ 缓存只增不减,调用参数一多就把内存吃满
cache = {}
def f(n):
if n not in cache:
cache[n] = n * n
return cache[n]
# ✅ 用 lru_cache 设个上限,超出自动淘汰最久未用的
from functools import lru_cache
@lru_cache(maxsize=128)
def g(n):
return n * n坑七:把defaultdict当只读字典。
# 适用于 Python 3.8+
# ❌ 一次读取就写入了新键
from collections import defaultdict
g = defaultdict(list)
if g["missing"]: # 读取时自动创建 'missing'
pass
# ✅ 只读用 in 或 get
if "missing" in g:
pass总结
| 场景 | 推荐做法 | 注意 |
|---|
| 配置合并 | {**defaults, **user}(3.9+ 可用 `\ | `) |
| 计数 | Counter或get(k, 0) + 1 | 缺键先给 0 |
| 分组 | setdefault(k, [])/defaultdict(list) | 列表是原地追加 |
| 缓存 | 手写 dict 或lru_cache | 键必须可哈希 |
| JSON | json.dumps/json.loads | 键只能是字符串 |
字典在实战里的价值,不在于它有多少方法,而在于它能把「对应关系」直接表达出来。用对它,配置、计数、分组、缓存这四类常见需求都会变得短而清晰;但一定要盯住两条红线:键必须可哈希,以及JSON 往返不保留非字符串键的类型。