ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python字典入门:键值对、遍历与嵌套实战

Python字典入门:键值对、遍历与嵌套实战 学完列表和元组很多刚开始学 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张三, age19) 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(1one) 这种写法会报错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(c5, d6)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 iterationd {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, keylambda 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模块的deepcopyimport 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这几个高频方法用顺其他方法用到再查完全来得及。等你能熟练写出“读取一段文本、统计词频、按频率排序”这类小脚本字典这关就算真正过了。
返回列表