page contents

Python字典:你以为你全会了,但这10个坑90%的人踩过

她要从一个嵌套字典里取用户信息,写了四层.get(),每层都带默认值。逻辑没问题,但那行代码长得像一列火车。我问她:你试过defaultdict吗?她摇头。又问:|运算符知道吗?还是摇头。

attachments-2026-08-mWFlTDkZ6a7143cdb432a.png

她要从一个嵌套字典里取用户信息,写了四层.get(),每层都带默认值。逻辑没问题,但那行代码长得像一列火车。我问她:你试过defaultdict吗?她摇头。又问:|运算符知道吗?还是摇头。

不是她的问题。Python字典太常用了,常用到所有人觉得"这玩意儿我早会了",但真到工程里,踩坑的一个接一个。

今天不讲d = {"name": "Tom"}这种基础。讲点你大概率没注意到、但在生产环境里真实会咬人的东西。

坑1:遍历字典时修改,直接炸

这个坑看起来低级,但在实际项目里出现的频率高得离谱。

你在遍历一个字典,发现某些key不满足条件,想顺手删掉:

user_scores = {"Alice": 85, "Bob": 42, "Charlie": 91, "Dave": 38}

for name, score in user_scores.items():
    if score < 60:
        del user_scores[name]

跑一下,RuntimeError: dictionary changed size during iteration。Python不允许你在遍历字典的同时修改它的结构,这是运行时保护。

解决方法有两种。第一种是先收集要删的key,遍历完再统一删:

to_delete = [name for name, score in user_scores.items() if score < 60]
for name in to_delete:
    del user_scores[name]

第二种更Pythonic,直接用字典推导式重建:

user_scores = {name: score for name, score in user_scores.items() if score >= 60}

 

第二种会创建新字典,数据量大的时候内存开销翻倍。如果字典特别大,用第一种。

坑2:setdefault和defaultdict,你该用哪个

这个场景极其常见:你要往字典里追加值,但不确定key存不存在。

新手写法:

if "tags" not in article:
    article["tags"] = []
article["tags"].append("Python")

五行变一行,用setdefault:

article.setdefault("tags", []).append("Python")

setdefault(key, default)的逻辑是:如果key存在,返回对应的值;不存在,就把default塞进去并返回。所以append直接追加到列表上。

但如果你整个字典的使用场景就是"值都是列表",每次访问都可能追加,那defaultdict更合适:

from collections import defaultdict

article_tags = defaultdict(list)
article_tags["Python基础"].append("字典")
article_tags["Python基础"].append("列表")
article_tags["数据分析"].append("pandas")

defaultdict(list)的意思是:访问任何不存在的key时,自动创建一个空列表。不需要判断、不需要setdefault,直接append。

区别在哪?setdefault适合"偶尔需要默认值"的场景,defaultdict适合"几乎每个key都需要默认值"的场景。如果你只有10%的key需要默认值,用setdefault更明确;如果90%的key都需要,defaultdict省掉大量重复代码。

坑3:字典的|运算符,3.9之后才有

合并两个字典,你大概率写过这种:

config = {**default_config, **user_config}

或者更老的写法:

config = default_config.copy()
config.update(user_config)

Python 3.9之后,可以用|运算符:

config = default_config | user_config

行为和{a, b}完全一致:右边的字典覆盖左边同名的key。还有一个|=做原地更新:

config = {"timeout": 30, "retry": 3}
user_override = {"timeout": 60}
config |= user_override

这个语法在处理配置合并、默认值覆盖时特别清爽。但注意,如果你的项目需要兼容3.8及更早版本,不能用。

坑4:字典有序这件事,3.7才保证

面试常问:Python字典有序吗?

答案是:3.7开始,字典保证插入顺序。3.6是CPython实现细节,没写进语言规范。3.5及之前,字典无序。

这意味着什么?

d = {}
d["zebra"] = 1
d["apple"] = 2
d["mango"] = 3

for key in d:
    print(key)

如果你的代码依赖遍历顺序(比如序列化成JSON后字段顺序需要固定),确保运行环境是3.7+。如果不确定,用OrderedDict:

from collections import OrderedDict

d = OrderedDict()
d["zebra"] = 1
d["apple"] = 2
d["mango"] = 3

但说实话,2026年了,大多数项目都跑在3.9+,普通字典就够了。OrderedDict存在的意义更多在于语义明确——看到OrderedDict你就知道这段代码依赖顺序,看到dict你不确定。

坑5:浅拷贝的陷阱

original = {"tags": ["Python", "AI"], "count": 10}
copied = original.copy()

copied["count"] = 20
copied["tags"].append("ML")

print(original["count"])   # 10,没问题
print(original["tags"])    # ["Python", "AI", "ML"],炸了

.copy()是浅拷贝。顶层key-value是独立的,但value如果是可变对象(列表、字典、集合),拷贝的只是引用。copied["tags"]和original["tags"]指向同一个列表,append操作两边都变了。

 

正确做法,用copy.deepcopy:

import copy

copied = copy.deepcopy(original)
copied["tags"].append("ML")

print(original["tags"])    # ["Python", "AI"],安全

deepcopy会递归拷贝所有层级。代价是慢——如果字典嵌套很深或包含大量数据,deepcopy可能成为性能瓶颈。在性能敏感的场景,考虑用不可变数据结构(tuple替代list,frozendict替代dict),从根源避免这个问题。

坑6:in运算符只查key,不查value

scores = {"Alice": 85, "Bob": 42}

print(85 in scores)  # False
print("Alice" in scores)  # True

in默认遍历的是key。想查value存不存在,要用.values():

print(85 in scores.values())  # True

但.values()在Python 3中返回的是view对象,in操作是线性扫描,O(n)复杂度。如果字典很大、查找频繁,建议反过来建一个value到key的索引:

score_to_name = {v: k for k, v in scores.items()}
print(85 in score_to_name)  # O(1)

坑7:get的默认值可以是任意对象,包括函数调用结果

name = user.get("nickname", "匿名用户")

name = user.get("nickname", expensive_db_query())

上面这行代码的问题:expensive_db_query()无论key存不存在都会执行。get的第二个参数是表达式,在函数调用前就求值了。

如果你的默认值计算成本高,用条件表达式:

name = user["nickname"] if "nickname" in user else expensive_db_query()

或者更简洁的写法,用try-except:

try:
    name = user["nickname"]
except KeyError:
    name = expensive_db_query()

try-except在key存在时没有额外开销,in检查需要多一次哈希查找。在性能敏感且key大概率存在的场景,try-except更快(EAFP原则:Easier to Ask Forgiveness than Permission)。

坑8:字典推导式的变量泄漏(3.7+已修复,但旧代码要注意)

Python 2里,列表推导式会泄漏循环变量到外层作用域。Python 3修复了这个问题,但字典推导式在某些早期3.x版本中仍有边缘情况。

squares = {x: x**2 for x in range(5)}
print(x)  # NameError: name 'x' is not defined

这不是什么大坑,但如果你维护的是从Python 2迁移过来的老代码,注意检查推导式里的变量名是否和外层冲突。

坑9:JSON序列化时,字典的key必须是字符串

import json

data = {1: "one", 2: "two"}
print(json.dumps(data))

JSON规范要求key必须是字符串。json.dumps会自动把int key转成string,但反过来json.loads不会自动转回去:

loaded = json.loads('{"1": "one", "2": "two"}')
print(loaded[1])  # KeyError: 1
print(loaded["1"])  # "one"

如果你的数据流是 Python → JSON → Python,int key会变成string key。这在REST API开发中特别烦——前端传来的数据key永远是字符串。

解决方法:在反序列化后做一次key转换:

loaded = {int(k): v for k, v in json.loads(json_str).items()}

或者从源头避免:设计数据结构时,字典key就用字符串。

坑10:dict不是万能的,有时候你该用NamedTuple或dataclass

这个坑不是bug,是设计问题。

很多Python开发者的习惯是:需要存一组关联数据?字典走起。

user = {"name": "Alice", "age": 30, "email": "alice@example.com", "department": "Engineering"}

然后用的时候user["name"]、user.get("emial")(拼错了,静默返回None,不报错)。

当字典的key集合是固定的、结构是已知的,它其实不是一个"字典",它是一个"对象"。这时候应该用dataclass:

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str
    department: str

user = User(name="Alice", age=30, email="alice@example.com", department="Engineering")
print(user.name)       # 属性访问,IDE有自动补全
print(user.emial)      # AttributeError: 'User' object has no attribute 'emial'

dataclass的好处:拼写错误在运行时立即暴露,IDE能自动补全属性名,类型检查器(mypy、pyright)能在编译期发现问题。字典的灵活性是它的优点,也是它的缺点——太灵活了,错误只能在运行时发现。

判断标准很简单:如果你能在纸上写出这个字典的所有key,用它的人都知道key叫什么,那它就不是字典,是一个被字典伪装的对象。换成dataclass,代码健壮性立刻提升一个台阶。

字典是Python里用得最多的数据结构之一。但"用得多"和"用得对"之间隔了十万八千里。

上面10个坑,有些是运行时炸弹(坑1、坑5),有些是性能黑洞(坑6、坑7),有些是可维护性慢性病(坑10)。不会让你的程序立刻崩掉,但会在某一天、某个不该出问题的时候,给你一个"惊喜"。

写代码这件事,不怕不会,怕的是"我以为我会了"。

字典如此,其他事也如此。

 更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

  • 发表于 2026-08-04 09:43
  • 阅读 ( 30 )
  • 分类:Python开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

2311 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. Pack 2311 文章
  3. 小柒 2228 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章