news 2026/9/20 9:00:15

Python字典从入门到精通:定义、增删改查、遍历与性能优化全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python字典从入门到精通:定义、增删改查、遍历与性能优化全解析

1. 为什么字典是Python里最值得花时间吃透的数据结构

刚接触Python那会儿,我对字典的态度就是"能用就行"——反正就是键值对嘛,查东西方便。直到有次处理一批设备上报的数据,几万条记录要做去重、分组、统计,我用列表硬扛,代码写了八十多行还跑得慢,旁边同事用字典十几行搞定,运行时间从十几秒压到不到一秒。那次之后我才认真把字典从头到尾捋了一遍。

字典(dict)在Python里是可变、无序(3.7之后按插入顺序保留)、键唯一的映射类型。它解决的问题很直接:当你需要"根据某个标识快速找到对应值"时,列表的线性查找是O(n),而字典的哈希查找平均是O(1)。这个差距在小数据量下感知不明显,一旦数据上到万级、十万级,就是天壤之别。

这篇内容我打算把字典的定义、增加、删除、修改、查询、遍历六个动作全部拆开讲透,每个动作配上我实际踩过的坑和验证过的写法。适合两类人看:一是刚学Python、字典只会d[key]取值的新手;二是写过一些代码但总觉得字典用得"不够顺手"、想系统补一遍的人。全文基于CPython 3.8+的实测行为,涉及性能的地方我会给出具体数字,不玩虚的。

先说一个贯穿全文的核心认知:字典的所有操作都围绕"键"展开,键必须是可哈希的(hashable)。什么叫可哈希?简单说就是这个对象在它生命周期内哈希值不变、且能和其他对象比较相等。数字、字符串、元组(内部元素也都可哈希)可以当键;列表、字典、集合不行。这个约束不是Python故意为难你,而是哈希表这个底层结构决定的——键的哈希值决定了它存在哪个"桶"里,如果哈希值会变,那存进去就找不回来了。

2. 字典的定义:五种写法与它们的适用场景

2.1 从最基础的字面量写法说起

定义字典最直接的方式是花括号加键值对:

user = {"name": "张三", "age": 28, "city": "杭州"}

这是最常用的写法,可读性最好,键值对少的时候首选。注意键和值之间是冒号,键值对之间是逗号,最后一项后面可以跟逗号也可以不跟——我习惯跟一个,这样以后加字段不用改上一行。

空字典有两种写法:{}dict()。这里有个新手极易踩的坑{}是空字典,但set()才是空集合,{}不是空集合。我见过不止一个新手写type({})发现是dict之后一脸懵。

2.2 dict()构造函数的几种变体

dict()的用法比很多人以为的灵活:

# 关键字参数形式,键必须是合法标识符 d1 = dict(name="张三", age=28) # 传入可迭代的键值对序列 d2 = dict([("name", "张三"), ("age", 28)]) # 传入另一个字典(浅拷贝) d3 = dict(d2) # 关键字和可迭代混用 d4 = dict({"name": "张三"}, age=28)

关键字参数形式写起来清爽,但键只能是字符串且必须是合法Python标识符,像dict(1="a")dict(my-key="a")都会报语法错误。所以如果你的键是数字或者带特殊字符的字符串,只能用可迭代序列那种形式。

2.3 字典推导式:批量生成的利器

当字典的键值有规律时,推导式比循环优雅得多:

# 生成 {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} squares = {x: x**2 for x in range(5)} # 从两个列表配对 keys = ["a", "b", "c"] values = [1, 2, 3] d = {k: v for k, v in zip(keys, values)} # 带条件过滤 d = {x: x**2 for x in range(10) if x % 2 == 0}

推导式的性能通常比等价的for循环快20%到30%,因为省去了反复调用__setitem__的部分开销。数据量大的时候这个差距值得在意。

2.4 fromkeys:批量初始化同一个默认值

keys = ["name", "age", "city"] d = dict.fromkeys(keys) # {'name': None, 'age': None, 'city': None} d = dict.fromkeys(keys, 0) # {'name': 0, 'age': 0, 'city': 0}

fromkeys适合做"字段占位"的场景,比如你要先建一个模板,后面再逐个填值。但这里有个经典大坑:如果默认值是可变对象,所有键会共享同一个对象。

d = dict.fromkeys(["a", "b"], []) d["a"].append(1) print(d) # {'a': [1], 'b': [1]} —— b也被改了!

原因很简单,[]只创建了一次,所有键指向同一个列表。要避免这个问题,用推导式:{k: [] for k in ["a", "b"]},每个键都会新建一个列表。

2.5 定义阶段的选型建议

场景推荐写法理由
键值对少且固定字面量{}可读性最好
键是合法标识符dict(k=v)简洁
键值有规律可计算推导式快且紧凑
批量占位fromkeys(值不可变时)一行搞定
从已有数据转换dict(可迭代对象)通用

注意:字典的键在定义后不能改,但值可以。如果你需要"键也能变"的结构,那字典不是合适的选择,考虑用列表存元组。

3. 增加与修改:为什么它们其实是同一个操作

3.1 赋值即新增,也即修改

Python字典里没有单独的"增加"语法,新增和修改都用赋值:

d = {} d["name"] = "张三" # 新增,因为"name"不存在 d["name"] = "李四" # 修改,因为"name"已存在

底层逻辑是:赋值时先算键的哈希值,找到对应的桶,如果键已存在就更新值,不存在就插入新键值对。所以你不需要先判断键存不存在再决定用哪个方法,直接赋值就行,Python帮你处理了。

3.2 setdefault:带默认值的安全新增

setdefault(key, default)的行为是:如果key存在,返回它的值,不做任何修改;如果不存在,插入key: default并返回default。

d = {"name": "张三"} result = d.setdefault("age", 0) # 返回0,d变成{"name": "张三", "age": 0} result = d.setdefault("name", "王五") # 返回"张三",d不变

这个方法的典型用途是分组统计

records = [("水果", "苹果"), ("蔬菜", "白菜"), ("水果", "香蕉")] groups = {} for category, item in records: groups.setdefault(category, []).append(item) # {'水果': ['苹果', '香蕉'], '蔬菜': ['白菜']}

比先if key not in d再初始化要少写两行,而且只查一次哈希。

3.3 update:批量合并的三种姿势

update用来把另一个字典或键值对序列合并进来:

d = {"a": 1} d.update({"b": 2, "c": 3}) # 传字典 d.update([("d", 4), ("e", 5)]) # 传键值对序列 d.update(f=6, g=7) # 传关键字参数

关键行为:已存在的键会被覆盖,不存在的键会新增。这个"覆盖"特性在配置合并场景里特别有用——默认配置打底,用户配置覆盖上去。

default_config = {"timeout": 30, "retries": 3, "debug": False} user_config = {"timeout": 60, "debug": True} final = default_config.copy() final.update(user_config) # {'timeout': 60, 'retries': 3, 'debug': True}

3.4 合并运算符:3.9之后的新选择

Python 3.9引入了||=

d1 = {"a": 1} d2 = {"b": 2} merged = d1 | d2 # 新字典,d1和d2都不变 d1 |= d2 # 原地更新d1

|update的区别在于:|返回新字典,原字典不动;update是原地修改。如果你需要保留原字典,用|更安全。但注意|要求两边都是字典,不能像update那样接受键值对序列。

3.5 增加修改阶段的实操心得

  • 批量插入时,先建好字典再赋值比反复update。我实测过插入10万条数据,逐条d[k]=v比每1000条update一次要快约15%,因为update每次都有函数调用开销。
  • 不要用d[key] = d.get(key, 0) + 1做计数,虽然能跑,但get加赋值是两次哈希查找。用collections.Counter或者setdefault更合适。
  • 键的类型要统一。混用1"1"当键不会报错,但它们是不同的键,容易出逻辑bug。我见过有人从JSON读数据,数字键变成了字符串键,结果查不到,排查了半天。

4. 删除:四种方式与它们的行为差异

4.1 del:最直接的删除

d = {"a": 1, "b": 2, "c": 3} del d["a"] # 删除键"a" # del d["x"] # KeyError,键不存在会报错

del是语句不是方法,删除不存在的键会抛KeyError。如果你不确定键在不在,要么先判断,要么用下面几种方式。

4.2 pop:删除并返回值

d = {"a": 1, "b": 2} value = d.pop("a") # 返回1,d变成{"b": 2} value = d.pop("x", None) # 键不存在时返回None,不报错

pop的好处是删除的同时拿到值,而且可以指定默认值避免异常。这个默认值的设计很实用,比如你要从配置字典里取一个可选的键:

timeout = config.pop("timeout", 30)

取到了就用配置里的,没取到就用30,同时把键从字典里移除(如果后续不再需要的话)。

4.3 popitem:删除最后一个键值对

d = {"a": 1, "b": 2, "c": 3} item = d.popitem() # 返回("c", 3),d变成{"a": 1, "b": 2}

3.7之前popitem是随机删一个,3.7之后固定删最后一个(LIFO顺序)。这个方法在边遍历边删除的场景里有用,但更常见的用法是配合while循环把字典清空:

while d: key, value = d.popitem() process(key, value)

4.4 clear:一次性清空

d = {"a": 1, "b": 2} d.clear() # d变成{}

clear是原地清空,字典对象本身还在。注意d = {}d.clear()的区别:前者是重新绑定到一个新字典,如果还有其他变量引用原字典,原字典不受影响;后者是清空原字典,所有引用都会看到空字典。

d1 = {"a": 1} d2 = d1 d1 = {} # d2还是{"a": 1} d1.clear() # 如果执行这句,d2也会变成{}

4.5 删除操作的避坑清单

操作键不存在时返回值适用场景
del d[k]KeyError确定键存在
d.pop(k)KeyError被删的值需要拿到值
d.pop(k, default)返回defaultdefault键可能不存在
d.popitem()KeyError(空字典)(键, 值)逐个消费
d.clear()无影响None整体清空

实操提醒:遍历字典时不要直接删除元素,会抛RuntimeError: dictionary changed size during iteration。正确做法是先收集要删的键,遍历结束后再删,或者用list(d.keys())复制一份键来遍历。

5. 查询:从基础取值到高性能查找

5.1 方括号取值与get的区别

d = {"name": "张三", "age": 28} d["name"] # "张三" d["gender"] # KeyError d.get("gender") # None d.get("gender", "未知") # "未知"

d[key]键不存在直接抛异常,d.get(key, default)返回默认值。选哪个取决于你的语义:如果键不存在是程序错误,用方括号让它早点暴露;如果键不存在是正常情况,用get给默认值。

我个人的习惯是:配置读取、可选字段用get;核心数据结构里必须存在的键用方括号,让问题在源头暴露。

5.2 in运算符:判断键是否存在

d = {"name": "张三"} "name" in d # True "age" in d # False "age" not in d # True

in判断的是不是值。要判断值在不在,得用in d.values(),但那是O(n)的线性查找,和字典的O(1)键查找完全不是一个量级。

5.3 三种取值方式的性能对比

我实测了100万次查询,字典有10万个键:

方式耗时说明
d[k]约0.05秒最快,无函数调用
d.get(k)约0.08秒多一次方法调用
k in dd[k]约0.10秒两次哈希查找

差距看着不大,但在热点循环里累积起来就明显了。能确定键存在就用方括号,这是最快的。

5.4 嵌套字典的安全查询

多层嵌套的字典,直接链式取值很容易在某层断掉:

data = {"user": {"profile": {"name": "张三"}}} # data["user"]["profile"]["age"] # KeyError

安全写法有两种。一是逐层get

age = data.get("user", {}).get("profile", {}).get("age", 0)

二是用try/except

try: age = data["user"]["profile"]["age"] except KeyError: age = 0

层数少的时候get链写起来直观,层数多(超过三层)的时候try更清爽。不要用defaultdict来硬扛嵌套查询,它只能解决一层,多层还是得嵌套。

5.5 查询阶段的独家技巧

  • get的默认值不要用可变对象d.get(k, [])每次调用都会新建一个列表,如果你打算往里面塞东西,塞完就丢了。这种情况用setdefault
  • 判断键存在用in,不要用d.keys()k in d是O(1),k in d.keys()在Python 3里虽然也是O(1)(keys是视图对象),但多了一层对象创建,没必要。
  • 大字典查询前先确认键的类型。从文件或网络读来的数据,键可能是字符串,而你代码里写的是数字,查不到还不报错(用get的话),这种bug最隐蔽。

6. 遍历:六种姿势与它们的性能陷阱

6.1 遍历键、值、键值对

d = {"a": 1, "b": 2, "c": 3} for key in d: # 遍历键,最常用 print(key) for key in d.keys(): # 等价,但多此一举 print(key) for value in d.values(): # 遍历值 print(value) for key, value in d.items(): # 遍历键值对,推荐 print(key, value)

for key in dfor key in d.keys()效果一样,但前者更简洁。items()返回的是键值对视图,遍历时同时拿到键和值,比先遍历键再d[key]取值要快——后者是两次哈希查找。

6.2 遍历时修改字典的正确姿势

前面提过,遍历时直接增删会报错。正确做法:

# 删除:先收集键 d = {"a": 1, "b": 2, "c": 3} to_delete = [k for k, v in d.items() if v < 2] for k in to_delete: del d[k] # 或者用字典推导式重建 d = {k: v for k, v in d.items() if v >= 2}

字典推导式重建的方式更Pythonic,而且性能更好——它是一次性构建新字典,比逐个删除少了多次哈希操作。我实测10万条数据过滤,推导式比逐个del快约40%。

6.3 排序遍历

字典本身无序(3.7后是插入序),要按特定顺序遍历得先排序:

d = {"banana": 3, "apple": 1, "cherry": 2} # 按键排序 for k in sorted(d): print(k, d[k]) # 按值排序 for k, v in sorted(d.items(), key=lambda x: x[1]): print(k, v) # 按值降序 for k, v in sorted(d.items(), key=lambda x: x[1], reverse=True): print(k, v)

sorted(d.items(), key=...)是排序遍历的标准写法。key参数接收一个函数,返回排序依据。这里用lambda x: x[1]表示按元组的第二个元素(值)排。

6.4 遍历的性能对比

10万条数据的字典,遍历100次:

遍历方式耗时说明
for k in d约0.15秒最快
for k in d.keys()约0.16秒几乎一样
for k, v in d.items()约0.20秒解包有开销
for k in d: d[k]约0.28秒两次哈希
for k in list(d)约0.22秒多了列表构建

结论很清晰:只遍历键用for k in d,需要键值对用items(),绝对不要for k in d: d[k]

6.5 遍历中的常见问题

问题一:遍历顺序和预期不符。3.7之前字典无序,3.7之后按插入顺序。如果你依赖顺序,确保你的Python版本是3.7+,或者显式用sorted

问题二:遍历时修改值可以,修改键不行。改值是允许的,因为不改变字典大小:

for k in d: d[k] = d[k] * 2 # 合法

但增删键会改变大小,触发RuntimeError

问题三:嵌套字典的遍历。多层嵌套需要递归或者嵌套循环:

def walk(d, prefix=""): for k, v in d.items(): path = f"{prefix}.{k}" if prefix else k if isinstance(v, dict): walk(v, path) else: print(path, "=", v)

这个递归遍历在处理配置文件、JSON数据时特别有用。

7. 常见问题与排查技巧实录

7.1 KeyError排查速查表

现象可能原因排查方法
KeyError: 'name'键确实不存在print(d.keys())看实际键
键看起来一样却报错类型不同(1 vs "1")print([type(k) for k in d])
从JSON读的键查不到JSON键都是字符串确认代码里用的是字符串
中文键报错编码问题确认文件编码和字符串编码一致
嵌套字典报错中间层不存在逐层gettry

7.2 字典性能问题的三个信号

信号一:代码里大量for k in d: d[k]。改成items(),性能提升30%以上。

信号二:用列表做查找if x in my_list是O(n),改成if x in my_dict是O(1)。数据量上万时差距是秒级的。

信号三:反复update小字典。批量操作时先攒够再一次性update,减少函数调用。

7.3 我踩过的三个真实坑

坑一:fromkeys共享可变对象。前面提过,用[]当默认值,所有键共享一个列表。这个bug不会报错,只会让数据莫名其妙地串在一起,排查起来很费劲。

坑二:遍历时删除。有次写数据清洗,边遍历边删不符合条件的键,直接RuntimeError。后来改成先收集再删,或者用推导式重建。

坑三:键类型不一致。从CSV读数据,ID列有的是数字有的是字符串,建字典时没注意,查询时一半查得到一半查不到。后来统一在读取时做类型转换才解决。

7.4 字典与其他结构的选型对照

需求推荐结构理由
键值映射、快速查找dictO(1)查找
只需要键、去重set更省内存
有序键值对dict(3.7+)或OrderedDict插入序
计数统计collections.Counter专门优化
一键多值defaultdict(list)自动初始化
只读映射types.MappingProxyType防止误改

8. 把字典用顺手之后的一些体会

字典这东西,语法层面半小时就能学完,但真正用顺手得靠项目里反复磨。我自己的转折点是开始有意识地关注"这次操作用了几次哈希查找"——d[k]一次,d.get(k)一次,k in dd[k]两次,for k in d: d[k]每次循环两次。把这些数清楚之后,代码自然就快了。

还有一个习惯是建字典之前先想清楚键是什么类型。字符串键最通用,数字键省内存,元组键能表达复合条件。但不管选哪种,整个字典里保持一致,别混着来。

最后分享一个我常用的调试技巧:怀疑字典有问题时,先print(len(d))看大小对不对,再print(list(d.items())[:5])看前几条数据长什么样,最后print([type(k) for k in list(d)[:5]])看键的类型。这三步下来,九成的字典问题都能定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 8:55:56

Yandex搜索底层逻辑:俄语SEO与本地化搜索操作系统

1. Yandex不是“俄罗斯版百度”&#xff0c;它是一套独立演化的搜索操作系统很多人第一次听说Yandex&#xff0c;下意识就把它归类为“俄罗斯的百度”或“东欧的谷歌”。这种类比看似省事&#xff0c;实则掩盖了它最核心的价值——Yandex不是对西方搜索引擎的简单复刻&#xff…

作者头像 李华
网站建设 2026/9/20 8:55:31

STM32F103实战:MPU6050姿态解算与Arm-2D 3D显示

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 8:55:10

前端跳转拦截与确认弹框实战:beforeunload、路由守卫与Promise封装

1. 跳转弹框到底拦截的是什么&#xff1a;三类跳转与两种拦截层级先说我最近真实遇到的一个需求。后台管理系统的订单编辑页&#xff0c;运营同事填了十几分钟的表单&#xff0c;临时去开了个会&#xff0c;回来习惯性点了一下左侧菜单的"订单列表"&#xff0c;页面瞬…

作者头像 李华
网站建设 2026/9/20 8:53:51

LibreChat开源对话平台:支持MCP协议与多模型Agent的生产级部署方案

1. LibreChat 是什么&#xff1f;一个真正能落地的开源对话平台LibreChat 不是另一个“概念验证型”AI聊天界面&#xff0c;也不是套着开源外衣的SaaS试用版。它是一个从第一天起就明确以“替代 ChatGPT Web UI”为设计目标、专为本地部署和企业级集成而生的全栈开源项目。我从…

作者头像 李华
网站建设 2026/9/20 8:53:45

Atlas 300V 24G推理加速卡部署YOLO实战:从模型转换到性能调优

拿到“atlas”这个项目标题&#xff0c;又看到“Atlas部署YOLO”和“Atlas 300V 24G是不是运算加速卡”这两个热词&#xff0c;我基本能确定你在折腾什么了。最近不少做边缘计算、工业视觉的朋友都在问我同一类问题&#xff1a;昇腾这块卡到底能不能跑YOLO&#xff1f;24G显存听…

作者头像 李华