学完列表和元组,很多刚开始学 Python 的同学都会卡在同一个地方:数据确实存进去了,但想取某个字段的时候,还得掰着手指头数“这个值是第几个”。举个例子,你在列表里存了一个学生信息,["001", "张三", 19, "13800000000"],过两天回来看这段代码,还得努力回想第0位是学号、第1位是姓名——这个心智负担会随着数据变多变重,代码也变得越来越难读。Python 里的字典(dict)就是来解决这个痛点的:它允许你给每个值起一个名字,然后直接按名字取数据,不用去关心它存放在了哪个位置。
这一篇咱们就把字典从头到尾过一遍。它大概是 Python 入门阶段性价比最高的一种数据结构,因为它不仅能存单个对象的信息,还能做统计、做映射、做嵌套的复杂结构,几乎所有后续学习都会用到它。比如解析接口返回的 JSON、读取配置文件、统计词频,背后全是字典。本文适合刚学完列表、想进一步提升的初学者,也适合想系统梳理字典知识点的同学。
1. 为什么需要字典:从“按名字找数据”说起
1.1 列表的局限性:靠序号记数据太反人类
列表本身没什么不好,它适合存放“同一类、顺序敏感”的数据,比如一组分数、一段文字。但当你需要用列表去表达“一个人的多维信息”时,问题就来了:
student = ["001", "张三", 19, "13800000000"] print(student[1]) # 输出"张三",但这个1的含义全靠你脑补代码里的student[1]到底代表姓名还是年龄?光看代码完全不知道。一旦这类列表多了,代码就变成了“只有写的人才能猜懂”的迷之数据。更要命的是并行列表的对应关系:
names = ["张三", "李四", "王五"] scores = [88, 92, 76] # 要找张三的成绩,你得先知道张三在第几个下标,再用同一个下标去取分数 idx = names.index("张三") print(scores[idx])这种写法有两个明显问题:第一,两个列表一旦不同步,数据就对不上了;第二,每次查找张三都得从头遍历一遍,数据量大了效率很难看。字典的诞生就是为了解决这两个问题。
1.2 字典的本质:键值对和查字典的思维模式
字典在 Python 里用dict表示,核心概念是“键值对”(key-value pair)。一个字典由若干“键”和对应的“值”组成,中间用冒号隔开,多个键值对用逗号分隔,整体用花括号包起来:
scores = {"张三": 88, "李四": 92, "王五": 76} print(scores["张三"]) # 88你可以把字典理解成一本“通讯录”或者纸质字典的检索表:键就是你要查的那个词条,值就是这个词条对应的解释。查字典的时候你按拼音直接翻到那个字,而不是从第一页开始逐页找;查通讯录的时候你直接找“张三”这个人,而不是把整本通讯录背下来。Python 字典做的事情就是这个:给你一个键,直接返回对应的值。
这里有几个底层逻辑要搞清楚:
- 键是唯一的。一个字典里不能出现两个相同的键,后写入的会覆盖先写入的,这个特性非常有用,天然适合做去重统计。
- 键必须是不可变类型。字符串、整数、浮点数、元组都可以当键,而列表这种可变类型不能当键,原因后面会专门讲。
- 键和值的关系是映射。它是“一个键对应一个值”的映射关系,而不是像列表那样的位置关系。这也就意味着,你不需要关心数据在字典内部到底怎么存放、在第几个位置,你只需要知道键是什么。
1.3 字典的核心应用场景
字典在实际项目里出现频率极高,入门阶段就需要有意识地往这几个场景上靠:
- 结构化信息:一个人的姓名、年龄、住址,用一个字典就能表达清楚,语义远强于列表。
- 映射与翻译:比如把城市名映射到区号、把错误码映射到错误消息。
- 统计计数:统计一段文本里每个单词出现几次,字典是天然工具。
- 配置信息:一个程序的配置项通常就是一组“参数名: 参数值”,读进来就是个字典。
- 接口数据处理:JSON 格式的数据解析成 Python 对象以后,最上层基本都是字典。
可以这么说,字典和列表加起来就是 Python 入门阶段最核心的两种“容器”,一个管位置顺序,一个管键名映射。先把字典的心智模型建立起来,后面的代码会顺很多。
2. 创建字典的四种方式,细节各有不同
2.1 花括号字面量:最常用也最直观
绝大多数情况下,你直接写花括号字面量就够了:
empty_dict = {} student = {"name": "张三", "age": 19} # 键是字符串时也可以写多行,末尾逗号可留可去 config = { "host": "127.0.0.1", "port": 8080, "debug": True, }这种方式的优点是直观:一眼就能看出哪个键对应哪个值。需要注意,{}创建的是空字典而不是空集合(集合要用set()),这两个很容易混。
字符串作为键时,不管你是用单引号还是双引号,规则都一样。但在dict()工厂函数里有一个特殊语法,直接用标识符写键名,不引号。
2.2 dict() 工厂函数:三种传参方式
dict()可以接受三种形态的参数,包括关键字参数、键值对序列、两个序列的对应关系:
# 第一种:关键字参数,键名必须是合法的标识符 d1 = dict(name="张三", age=19) print(d1) # {'name': '张三', 'age': 19} # 第二种:可迭代的键值对序列,比如列表里的元组 d2 = dict([("name", "张三"), ("age", 19)]) print(d2) # {'name': '张三', 'age': 19} # 第三种:用 zip 把两个序列拉链起来 keys = ["name", "age"] values = ["张三", 19] d3 = dict(zip(keys, values)) print(d3) # {'name': '张三', 'age': 19}第一种方式写起来方便,但键必须是“像变量名”一样的字符串;如果键本身是数字、包含空格或者有特殊字符,就不能这么写了,得退回第二种或字面量方式:
d4 = {1: "one", "my-key": "value"} # dict(1="one") 这种写法会报错zip配合dict是处理“两个平行列表合并成字典”的经典写法。有时候你从 CSV 文件或者 Excel 里读出来两列数据,一列是键、一列是值,直接dict(zip(keys, values))一行搞定,比写循环清爽得多。
2.3 fromkeys:一次创建多个键,共用同一个值
如果有一组键,你想让它们初始都指向同一个值,可以用fromkeys类方法:
subjects = ["语文", "数学", "英语"] scores = dict.fromkeys(subjects, 0) print(scores) # {'语文': 0, '数学': 0, '英语': 0}这里有个极易踩的坑:第二个参数如果传的是可变对象,比如空列表,它并不会给每个键拷贝一份列表,而是让所有键都指向同一个列表对象。一旦你给其中一个键追加数据,其他键也会跟着变:
d = dict.fromkeys(["a", "b"], []) d["a"].append(1) print(d) # {'a': [1], 'b': [1]},b也带上1了所以我的建议是:fromkeys只适合给所有键设同一个不可变默认值(比如0、空字符串、None),如果默认值需要是列表、字典这种可变结构,请改用后面的setdefault或者循环赋值。
2.4 用 zip 组合两个列表构建字典
刚才已经见到了dict(zip(...)),这里再补一个实用技巧:如果你需要对 zip 出来的数据进行加工,可以直接套列表推导式或字典推导式。比如两个列表长度不一致时,zip会自动按短的截断:
cities = ["北京", "上海", "广州", "深圳"] codes = ["010", "021", "020"] phone_code = dict(zip(cities, codes)) print(phone_code) # {'北京': '010', '上海': '021', '广州': '020'},深圳被忽略了这个截断行为在某些场景下是特性,在另一些场景下是坑:如果你希望严格校验长度,需要先确认len(cities) == len(codes)。入门阶段先知道有这种行为就行,真遇到长度对不齐的问题时能反应过来。
3. 字典的增删改查:核心操作逐个说清
3.1 查询:中括号与 get 的取舍
字典最常见的操作就是根据键取值,Python 提供了两种不同的写法,用途有细微差别:
d = {"name": "张三", "age": 19} print(d["name"]) # 张三 # print(d["city"]) # KeyError: 'city',键不存在直接抛异常 print(d.get("name")) # 张三 print(d.get("city")) # None,键不存在返回 None 而不是报错 print(d.get("city", "未知")) # 未知,可以指定找不到时的默认值这两者的选择原则很简单:当你确信键一定存在时用d["key"];当键可能不存在、你想优雅地处理时用d.get("key", 默认值)。比如一段文案里取人名,这个名字理论上一定存在,用中括号没问题;但如果是用户可能没填的可选字段,用get更安全。
还有一个和get类似的兄弟方法setdefault,它在键不存在时会把默认值写入字典,然后返回这个值;键存在时直接返回已有值,不做任何修改:
word_count = {} word_count.setdefault("hello", 0) print(word_count) # {'hello': 0} word_count["hello"] = 3 word_count.setdefault("hello", 0) # 键已存在,不会覆盖 print(word_count["hello"]) # 3setdefault在“统计分组”场景非常好用,后面嵌套字典那节会给出实例。
判断一个键是否存在,用in运算符,这是最推荐的写法:
if "name" in d: print("存在")3.2 新增与修改:同一个语法,两种行为
给字典加一个新键值对,和修改一个已有键的值,用的是同一个语法d[key] = value。判断逻辑由字典自己完成:
- 如果
key不存在,就执行“新增”; - 如果
key已经存在,就执行“修改”。
d = {} d["name"] = "张三" # 新增 d["age"] = 19 # 新增 d["age"] = 20 # 修改,age 的值从19变成20 print(d) # {'name': '张三', 'age': 20}这个“有则改、无则增”的二元行为,在统计词频时特别顺手:每次遇到一个单词,你就counts[word] = counts.get(word, 0) + 1,一行代码完成了“取旧值、加一、写回去”的整套流程。连续多次对同一个键赋值,最终只保留最后一次的值,这也是字典天然具备去重能力的原因。
3.3 删除:del、pop、popitem、clear 怎么选
删除操作有四种写法,它们的行为和适用场景各不相同:
d = {"name": "张三", "age": 19, "city": "上海"} # 1. del:直接删除,没有返回值,键不存在会报错 del d["city"] # del d["city"] # 再删除一次就 KeyError 了 # 2. pop:删除并返回被删除的 value age = d.pop("age") print(age) # 19 # 3. popitem:删除并返回最后插入的键值对 last = d.popitem() print(last) # 键值对以元组形式返回;在3.7+中返回的是最后插入的一项 # 4. clear:清空整个字典 d.clear() print(d) # {}实际开发里的选择通常是这样:
del d["key"]最常用,适合明确删除某个键且不需要返回值;d.pop("key", None)更安全,适合不确定键是否存在的情况,第二个参数是找不到时的默认值,如果不给默认值会报错;d.popitem()主要用在需要“逐个消费并移除”的场景,比如做一个待办列表,每处理完一项就从字典里弹出去;d.clear()很少用,主要用在重置状态的场景,比如清空缓存。
3.4 update:批量合并与覆盖规则
如果需要把一个字典的键值对批量并入另一个字典,用update方法:
d1 = {"a": 1, "b": 2} d2 = {"b": 3, "c": 4} d1.update(d2) print(d1) # {'a': 1, 'b': 3, 'c': 4}update的规则是:用参数里的字典去“覆盖”原字典中已有的键,同时把原字典没有的键追加进来。b原来的值 2 被参数中的 3 覆盖了,同时增加了c。
update也可以接收关键字参数、键值对序列:
d1.update(c=5, d=6)Python 3.9 开始,字典还支持用|和|=运算符做合并,写起来更符合直觉:
merged = d1 | d2 # 生成新字典,d1 和 d2 都不变 d1 |= d2 # 等价于 d1.update(d2)如果你的运行环境是 3.9+,我建议直接拥抱这个新语法。如果是要兼容老版本,还是老老实实用update。
3.5 视图对象:keys、values、items 是什么
字典提供了三个视图方法:keys()返回所有键、values()返回所有值、items()返回所有键值对。它们返回的不是普通的列表,而是“视图对象”,视图有一个很有意思的性质:它会跟随字典的变化而动态变化。
d = {"a": 1, "b": 2} ks = d.keys() print(list(ks)) # ['a', 'b'] d["c"] = 3 print(list(ks)) # ['a', 'b', 'c'],视图自动跟着变了如果你真的需要一份“独立的键列表”来规避这种动态性,显式转成list即可:
keys_snapshot = list(d.keys())items()返回的每个元素是一个(key, value)元组,在遍历时会大量使用。
4. 遍历字典:不同的遍历目标对应不同写法
4.1 只遍历键和只遍历值
遍历键是最常见的操作,直接for key in d就能拿到所有键,不需要显式调用keys():
d = {"a": 1, "b": 2, "c": 3} for k in d: print(k) # 依次输出 a b c这只是语法糖层面简化,本质上和for k in d.keys()是一致的。只遍历值可以用values():
total = 0 for v in d.values(): total += v如果你的目的是“值的总和”“最大值”这类聚合操作,直接用内置函数sum(d.values())、max(d.values())更简洁。Python 的哲学是能用一行表达的事就不写长循环。
4.2 同时拿键和值:d.items() 的标准姿势
当你在循环里既需要键又需要值时,不要写“先取键再查值”的模式:
for k in d: print(k, d[k]) # 功能对,但重复索引不够优雅更推荐的写法是直接解包items():
for key, value in d.items(): print(key, value)因为items()返回的每个元素是二元元组,用两个变量直接解包是 Python 的惯用写法。这种结构在推导式里也同样适用。
4.3 遍历时修改字典:一个容易踩的 RuntimeError
很多人会在这个地方翻车。循环遍历字典的过程中,如果你往里加键或者删键,会抛出RuntimeError: dictionary changed size during iteration:
d = {"a": 1, "b": 2} for k in d: d.pop(k) # RuntimeError: dictionary changed size during iteration但是如果你在遍历时只修改值、不改变字典的“结构”(也就是不新增和删除键),是没问题的:
for k in d: d[k] = d[k] * 2 # 只改值,不增删键,合法如果需要一边遍历一边删键,正确姿势是先把键的列表拍个快照,再在这个快照上遍历:
for k in list(d): if d[k] < 2: d.pop(k)list(d)会一次性把所有键拷贝成一个普通列表,后续遍历的是这个独立列表,不再受字典结构变化影响。这个坑在清理无效数据时经常遇到,建议提前记住。
4.4 按排序输出键值对
字典本身不保证你有序访问按照键的大小顺序,但想按某种顺序遍历很简单:
for k in sorted(d): print(k, d[k])同理,按值排序需要用sorted加key参数:
# 按值从小到大遍历 for k in sorted(d, key=lambda k: d[k]): print(k, d[k])这一套在后续处理统计数据、排行榜类需求时很常用。入门阶段先掌握sorted(d)按键排序基本就够用了,按值排序可以作为进阶练习来理解。
5. 字典推导式:一行代码完成数据加工
5.1 基础形式:从循环到推导式
字典推导式的语法和列表推导式很像,只不过外层花括号里写的是键: 值:
squares = {i: i ** 2 for i in range(1, 6)} print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}换成普通循环要三行:
squares = {} for i in range(1, 6): squares[i] = i ** 2推导式最大的价值是“读起来一目了然”:你一眼就能看出它在生成一个“从数到平方数”的字典。初学者可以先从等价的循环版本开始理解,写熟练后再压缩成推导式。
5.2 带条件的推导式:过滤一步到位
推导式后面可以跟if,实现“只保留满足条件的键值对”:
d = {"a": 1, "b": 2, "c": 3, "d": 4} even_values = {k: v for k, v in d.items() if v % 2 == 0} print(even_values) # {'b': 2, 'd': 4}也可以在for循环前面对“键”或“值”做加工,比如把键转成大写、值翻倍:
transformed = {k.upper(): v * 10 for k, v in d.items()}5.3 用 zip 快速重建字典
如果有两个列表,一个存名字、一个存分数,你可以用推导式做更灵活的控制:
names = ["张三", "李四"] scores = [88, 92] score_map = {name: score for name, score in zip(names, scores)}如果只是单纯把两个列表组合成字典,dict(zip(names, scores))更直接。但推导式的优势在于你可以顺手加过滤和加工逻辑,比如只保留及格的人:
passed = {name: score for name, score in zip(names, scores) if score >= 60}5.4 反转键值对
把键和值互换,是刷题和实际业务里都常遇到的操作:
d = {"a": 1, "b": 2} reversed_d = {v: k for k, v in d.items()} print(reversed_d) # {1: 'a', 2: 'b'}这里有两个隐蔽的注意点:
- 值必须可哈希。如果原来的值是列表,就不能直接当键,需要先转成元组。
- 值如果有重复,后面的会覆盖前面的。比如
{"a": 1, "b": 1}反转后会得到{1: 'b'},'a'被覆盖了。真遇到这种场景,你需要想清楚是接受覆盖,还是把多个键收集到一个列表里。
6. 嵌套字典:真实业务中的“表中有表”
6.1 字典里套字典:配置信息的经典形态
当信息开始有层级关系,嵌套字典就登场了。典型场景是配置文件:
config = { "database": { "host": "127.0.0.1", "port": 3306, "user": "root", "password": "123456", }, "debug": True, "log_level": "INFO", }访问嵌套字典的关键是逐层向下:
host = config["database"]["host"] print(host) # 127.0.0.1这种结构其实和 JSON 几乎完全一致。你在接口返回的数据里看到的每一个“花括号套花括号”,本质就是一串嵌套字典。把嵌套字典读好,等于把接口数据读好了一半。
6.2 字典与列表混合使用:学生成绩统计实例
更常见的情况是“列表套字典”或“字典套列表”。比如一个班的学生信息,最适合的结构是列表,因为学生之间是有先后顺序的;每个学生又是一份字典,因为每个学生内部是键值映射:
students = [ {"name": "张三", "scores": {"语文": 88, "数学": 92}}, {"name": "李四", "scores": {"语文": 77, "数学": 85}}, {"name": "王五", "scores": {"语文": 95, "数学": 78}}, ]想统计所有人的数学平均分,一行生成器表达式就够了:
math_sum = sum(s["scores"]["数学"] for s in students) math_avg = math_sum / len(students) print(math_avg) # 85.0这里注意访问链:students是列表 → 用遍历拿到每个学生的字典 → 通过s取scores键 → 再取数学键。每一层的类型要想清楚:遍历列表得到字典,字典取值得到另一个字典或普通值。
6.3 多层嵌套的安全访问:get 的连环用法
嵌套层级多了以后,直接config["database"]["host"]一旦上层键不存在,就会抛KeyError。安全做法是用get层层兜底:
host = config.get("database", {}).get("host", "localhost")当config里没有database时,第一层get返回空字典{},第二层get在空字典上取host,拿不到就返回默认值"localhost"。这种写法虽然略繁琐,但在解析不可靠的外部数据时非常有用。
另一个高频场景是用setdefault构建嵌套结构。比如把一组单词按首字母分组:
words = ["apple", "banana", "apricot", "blueberry", "cherry"] groups = {} for word in words: groups.setdefault(word[0], []).append(word) print(groups) # {'a': ['apple', 'apricot'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}setdefault在这里的语义是:如果这个首字母还不存在就新建一个空列表,然后取得这个列表,把单词追加进去。手动写法要判断两次,而setdefault把整个过程压缩成了一行,是嵌套字典构建里的头号利器。
6.4 JSON 和字典的关系
日常开发里,字典和 JSON 几乎是“换了个马甲”的关系。接收接口数据时用json.loads()解析,得到的就是字典;发送数据时用json.dumps()把字典转回 JSON 字符串。也就是说,你在网页接口里看到的复杂嵌套数据,到 Python 程序里就是套娃的字典、列表。学会字典,等于学会了和大多数 Web 服务打交道的基础能力。
7. 这些坑我替你们踩过:键、拷贝、默认值
7.1 键要可哈希:list 为什么不能当键
前面反复强调“键必须是不可变类型”,但真正写代码时才有人会翻车:
bad_dict = {["a", "b"]: 1} # TypeError: unhashable type: 'list'原因是字典底层是靠“哈希值”来组织和查找键的,而哈希值在对象生命周期内必须保持不变。列表是可变的,你刚拿它当键存进去,下一秒原地追加一个元素,它的哈希值就变了,字典内部的存储索引也就乱套了。字符串、整数、元组这类不可变对象没有这个问题,所以才能当键。
实际影响没那么抽象:比如你想把“一组坐标”映射到某个值,元组(1, 2)可以当键,列表[1, 2]不行。遇到这种需求时顺手把列表转成元组就行:
d = {(1, 2): "point"}7.2 顺序问题:字典到底有没有序
这是个历史问题,也是面试训练里常被翻出来的点。在 Python 3.5 及更早版本里,字典不保证键值对的顺序,你插入的顺序和遍历出来的顺序可能完全不一样;3.6 版本里 CPython 的实现让字典保持插入序,这本来只是个实现细节;3.7 版本开始,语言规范正式保证“字典保持插入顺序”。
所以现在的 Python 环境(3.7+)里,你可以直接依赖“后插入的键排后面”这个顺序:
d = {"a": 1, "b": 2, "c": 3} print(list(d)) # 输出 ['a', 'b', 'c']但如果你要兼容 3.5 及更早的代码,这个顺序不能依赖。建议平时开发不管环境如何,都不要写“依赖字典顺序做严格排序”的逻辑,真需要明确顺序场景那就不如直接用OrderedDict来得踏实。入门阶段只需要知道这点即可,不深究。
7.3 fromkeys 的默认值陷阱与 copy 的浅拷贝陷阱
这两个坑放在一起说,因为它们都源于同一个底层概念:Python 变量的赋值本质是引用的传递,不是值的复制。
先看fromkeys的坑,前面已经举过例子:所有键共享同一个可变默认值。再看copy的坑:
original = {"person": {"name": "张三"}} copied = original.copy() copied["person"]["name"] = "李四" print(original["person"]["name"]) # 李四,原字典也被改了!copy()是浅拷贝:它复制了最外层的字典,但外层的值如果是一个可变对象,新的字典和旧字典里的内层对象仍然是同一个。想要完整地复制所有嵌套层,必须用copy模块的deepcopy:
import copy deep_copied = copy.deepcopy(original) deep_copied["person"]["name"] = "王五" print(original["person"]["name"]) # 还是李四,不受影响判断的依据很实用:如果你的字典只是单层键值对,copy()就够了;只要出现“字典里面套列表、套字典”这种嵌套结构,需要独立副本就用deepcopy。
还有一个相关的基础坑:直接赋值并不会复制字典,只是造了一个“别名”:
a = {"x": 1} b = a # b 和 a 指向同一个字典 b["x"] = 100 print(a["x"]) # 100这一点初学者经常会忽略,一旦需要在函数里“保护”外部字典不被修改,就要格外留意。
7.4 进阶工具:defaultdict 与 Counter
入门阶段的主线是原生字典,但有两个collections模块里的工具,几乎是字典的“官方增强版”,提前认识能少写很多样板代码。
defaultdict可以指定工厂函数,当访问一个不存在的键时,它会自动先创建默认值再返回,省去了get或setdefault的麻烦:
from collections import defaultdict counts = defaultdict(int) for ch in "hello": counts[ch] += 1 print(counts) # defaultdict(<class 'int'>, {'h': 1, 'e': 1, 'l': 2, 'o': 1}) print(counts["x"]) # 直接返回 0,而不报 KeyErrorCounter则是“计数”场景的专用选手:
from collections import Counter cnt = Counter("hello") print(cnt.most_common()) # [('l', 2), ('h', 1), ('e', 1), ('o', 1)]看到统计词频的需求,优先考虑Counter,而不是自己手写循环加if判断。这类工具不是超纲内容,而是真正能提升编码效率的“字典周边配件”。
字典这一篇的内容量不小,从最基础的创建、增删改查,到遍历、推导式、嵌套,再到最后这些容易踩的坑,基本覆盖了入门阶段所有高频知识点。我个人在实际教学和带新人的过程中还有一个体会:别急着把所有方法一次性背下来,先抓住“键找值”这个核心模型,然后把get、items、setdefault这几个高频方法用顺,其他方法用到再查完全来得及。等你能熟练写出“读取一段文本、统计词频、按频率排序”这类小脚本,字典这关就算真正过了。