昨天晚上我在家里蹲着改一个小脚本,哎就是那种“跑一跑就完事”的,结果跑着跑着突然慢得离谱,我还以为又是网卡抽风…后来一看,哦豁,问题就出在我自己写的 dict 遍历上面,写得跟绕口令一样,循环里还各种重复查 key,对吧,这种代码平时不炸,数据一大就开始阴阳怪气。
你们肯定也干过这种事:for k in d: 进去以后又 d[k],再来个 if k in d:,再来个 d.get(k)…反正就是“我明明已经拿到钥匙了,还要回家翻抽屉找钥匙”,离谱。
先把最常见的几种写法摆一下,别整太玄学啊,真的就这几个姿势:
user_score = {"a": 10, "b": 20, "c": 30}
# 1) 默认遍历 key(最省字,但别在里面乱查)
for uid in user_score:
score = user_score[uid]
# ... do something
# 2) 同时拿 key + value(我日常最推荐这个,少一次查表)
for uid, score in user_score.items():
# ... do something
# 3) 只要 value
for score in user_score.values():
# ... do something
# 4) 只要 key 的时候还想排序(比如输出报告)
for uid in sorted(user_score):
print(uid, user_score[uid])
你看第 2 个 items(),就很关键:少一次 user_score[uid] 的哈希查找。别小看这一下,循环 10 万次、100 万次的时候,差距就出来了。群里经常有人问“为啥我遍历 dict 比 list 慢”,你先别怪 Python,先看看自己是不是在循环里重复查了两遍。
然后我那天遇到的坑更土:我一边遍历,一边删 dict 里的东西。嗯…你们懂的,Python 直接给你来一句运行时错误,像在说“哥,别闹”。
正确姿势是:要删就先“拍个快照”,别在迭代的那条河里搬石头。
sessions = {"u1": 3, "u2": 0, "u3": 1, "u4": 0}
# 错误示范:遍历时修改大小(别学)
# for k, v in sessions.items():
# if v == 0:
# del sessions[k]
# 正确:先拷一份要遍历的 key(或 items)
for k, v in list(sessions.items()):
if v == 0:
del sessions[k]
print(sessions) # {'u1': 3, 'u3': 1}
我当时还嘴硬说“我就删两个怎么会出事”,结果线上数据一多就崩,尴尬得很…所以这条你们记死:遍历的时候别改 dict 的大小。要改就 list(...) 一下,或者把要删的 key 先收集起来,第二轮再删。
还有一个很生活的场景:你拿到一个 dict,里面 value 是 dict(比如接口返回),你想过滤、改名、挑字段。很多人写三层 for,写到最后自己都不认识自己。
我一般就用 items() + 推导式,简洁还不容易写错,反正“像收拾桌面一样,一次把垃圾捡出来”。
resp = {
"u1": {"name": "Tom", "age": 18, "city": "BJ"},
"u2": {"name": "Ana", "age": 16, "city": "CD"},
"u3": {"name": "Li", "age": 21, "city": "SZ"},
}
# 只要 age >= 18 的,顺便把字段扁平化
adult = {
uid: {"name": info["name"], "age": info["age"]}
for uid, info in resp.items()
if info.get("age", 0) >= 18
}
print(adult) # {'u1': {'name': 'Tom', 'age': 18}, 'u3': {'name': 'Li', 'age': 21}}
你看这里 info.get("age", 0) 也挺实用的,接口偶尔缺字段,你别一上来就 info["age"],缺了直接 KeyError,生产同事大半夜找你喝茶,谁顶得住啊。
再说个我自己特别爱用的小技巧:循环里把 .get 或 .items 先“绑到局部变量”。听起来像玄学对吧,但它真能少一点属性查找开销。别指望这招让你从 10 秒变 0.1 秒哈,但在很热的循环里,能省一点是一点。
defsum_scores(d: dict[str, int]) -> int:
items = d.items # 先绑定方法引用
total = 0
for _, v in items():
total += v
return total
你要是写那种日志聚合、计数器的脚本,dict 遍历真是家常便饭。比如我那天就是做“按错误码统计次数”,一开始写得很丑:先遍历 list,再 if code in counter,再加一。其实用 get 一行就顺了。
logs = ["200", "200", "500", "404", "500", "500", "200"]
counter: dict[str, int] = {}
get = counter.get # 绑一下,懒得每次点来点去
for code in logs:
counter[code] = get(code, 0) + 1
# 遍历输出,按次数从高到低
for code, cnt in sorted(counter.items(), key=lambda x: x[1], reverse=True):
print(code, cnt)
对了,还有个细节,很多人没意识到:现在的 dict(Python 3.7+)是保持插入顺序的。也就是说你按什么顺序塞进去,大概率就按那个顺序遍历出来。做“配置覆盖”这类事就很舒服,但你别把它当数据库排序哈,想要稳定排序就用 sorted(),别靠“运气”。
最后我再插一句很真实的:写代码别一上来就追求“最优雅”,先保证你遍历 dict 的姿势别把自己绊倒。默认用 .items(),要改大小就 list(...) 快照,value 里拿字段就 get 防缺失,剩下的优化…等你真卡了再说,不然你会跟我一样,半夜对着那几行 for 循环骂骂咧咧。
行了我先不扯了,我这边水烧开了…哦对,谁要是还在循环里 if k in d: d[k] 这种写法,真的,改掉,别等它来教育你。