我在带新手学 Python 的时候发现一个很有规律的现象很多人学到列表list时觉得 Python 太好用了但一碰到字典dict就开始发懵。其实字典一点儿都不难难的是大家没想明白一个关键问题——字典到底解决了列表解决不了的那件什么事。这篇文章准备用最直白的例子把 Python 字典的创建、读取、修改、遍历、嵌套和常见坑全部过一遍非常适合刚学完列表、元组准备接触 dict 的初学者。就算你已经写过一阵子 Python但心里总觉得字典用过但没真正弄懂这篇文章也能帮你把概念理顺。学字典最好的方法不是背 API而是先理解它的底层逻辑它是一种键值对结构一切操作都在围绕着给一个名字快速找到对应的值展开。下面我就从一个真实需求出发一步步拆给你看。1. 列表解决不了的按名字找值正是字典诞生的理由先看一个特别常见的场景现在要记录三个同学的成绩名字分别是 alice、bob、carol分数分别是 85、92、78。用列表存的话第一反应可能是scores [85, 92, 78]这样存没问题但你想取出 alice 的分数时尴尬就来了scores[0]确实是 85可 85 是 alice 的吗你得在旁边再维护一个同学名单names [alice, bob, carol] scores [85, 92, 78]然后你每次查一个人的分数都得先在names里找到他的下标再去scores里取对应位置的数。这种两个列表按下标对齐的方案被称为并行列表反模式只要中间删掉一个人整个对齐关系就崩了。而字典就是专门来解决这个问题的score_dict {alice: 85, bob: 92, carol: 78} print(score_dict[alice]) # 85score_dict[alice]一句话定位不用数下标不用维护对齐关系。这才是字典存在的根本动机它是名字到值的映射表。1.1 字典的三个核心概念字典由一个个键值对组成叫 entry。键就是那个名字值就是名字对应的数据。这里有三条底层规则你必须刻在脑子里键必须唯一。同一个键重复出现时后面的值会把前面的值覆盖掉而且不报错。键必须不可变。字符串、数字、元组都可以当键列表这种可变对象不行因为字典靠键的哈希值来存储定位键一变位置就乱了。值可以是任何东西。数字、字符串、列表、元组、另一个字典都可以作为值。用一个生活类比帮助理解字典就是手机通讯录。你输入联系人姓名键立刻得到电话号码值。你在通讯录里不可能存两个完全相同的名字名字本身也不太会频繁修改这些特征和 Python 字典的约束完全一致。1.2 创建字典的三种常见方式了解了为什么需要字典之后直接上手。创建字典最常用的三招分别是空字典、直接写键值对、用dict()从别的结构转换# 方式一空字典 empty1 {} empty2 dict() # 方式二最常见的字面量写法 phone {alice: 13800001111, bob: 13900002222} # 方式三用 dict() 接收一个二元组列表 phone2 dict([(alice, 13800001111), (bob, 13900002222)])第一行{}是空字典注意它和空集合set()完全是两码事。{a: 1}是字典{a}是集合新手经常在这里混淆。方式三用得相对少但在从数据库查询结果、配置文件转成字典时特别好用。2. 创建、读取、修改、删除四板斧跑通最小例子任何容器类型的基本功都是增删改查字典也不例外。这一节我会用很短的例子把四个操作一次讲清楚。2.1 读取值方括号和 get 方法的本质差异先设一个最小字典scores {alice: 85, bob: 92}读取可以直接用方括号print(scores[alice]) # 85这里有个新手最容易踩的坑如果访问一个不存在的键Python 会直接抛异常程序当场崩掉。scores[tom] # KeyError: tom为了避免这种崩溃字典提供了get方法score scores.get(tom) # 返回 None不报错 score scores.get(tom, 0) # 返回默认值 0get的第二个参数就是查不到时用的兜底值。方括号是硬取必须保证键存在get是软取适合预期可能查不到的场景。两者的取舍原则很简单确定键一定存在用方括号不能确定用get。2.2 修改与新增同一套写法字典的写操作有一个非常友好的特性不管键存不存在都是等号赋值。键存在就是修改键不存在就是新增scores[bob] 95 # 把 bob 的分数改成 95 scores[dave] 88 # 新增 dave print(scores) # {alice: 85, bob: 95, dave: 88}一次更新多个键值对用update方法scores.update({alice: 99, eve: 70}) # 也可以传入二元组列表 scores.update([(alice, 100), (frank, 60)])update的语义是把传入字典里的每一对都批量执行修改或新增操作。相当于一个循环赋值但性能更好、写法也更干净。2.3 删除del、pop、popitem、clear 四个工具的取舍删除操作有四种常见方式初学者容易混# 1. del按键删除不存在会 KeyError del scores[bob] # 2. pop删除并返回被删的值可以给默认值 score scores.pop(alice, 0) # 返回 85键不存在时返回 0 # 3. popitem删除最后一对返回 (key, value) 元组 pair scores.popitem() # 4. clear清空字典 scores.clear()我的经验是需要用到被删掉的“值”时用pop这样可以顺手把结果存进变量不需要返回值时用del就够想把整个字典重置为空用clear。这里做个直观对比操作写错时表现返回值适用场景方括号d[k]键不存在抛 KeyError对应值确定键一定存在d.get(k, x)不报错返回 x对应值或默认值不确定键是否存在del d[k]键不存在抛 KeyError无删除且不要返回值d.pop(k, x)不报错返回 x被删的值删除同时要拿到值2.4 判断键是否存在用 in不是遍历如果只是想看一下键在不在用in运算符if alice in scores: print(alice 的成绩在册)注意in判断的是键不是值。如果你想判断某个分数有没有人考过就得遍历值那是后面的事情。这一小节总结下来就是字典的读、写、删都有对应的确定性 API真正要用熟其实写一遍通讯录增删改查就够了。3. 遍历字典的正确姿势直接遍历、keys、values、items 到底该用哪个新手遍历字典时最容易产生一个困惑for循环里到底拿到的是键还是值其实规则非常简单。3.1 直接遍历拿到的是键info {name: xiaoming, age: 18} for item in info: print(item) # 输出 # name # agefor item in info等价于for item in info.keys()遍历的是所有键。这是很多人第一次见识字典时感到意外的点咦不是应该循环出值吗不是的键才是字典的主索引默认遍历自然从键开始。3.2 三种视图方法的选择字典提供三个视图方法for k in info.keys(): # 遍历所有键 print(k) for v in info.values(): # 遍历所有值 print(v) for k, v in info.items(): # 同时拿到键和值 print(k, v)items()是最常用的因为它一次性把键值对解包成了两个变量。这三个方法返回的不是列表而是视图对象view object。视图最鲜明的特点是它会跟着原字典实时变化你先把视图存下来之后往字典里加了一个键再去遍历这个视图新键也会出现。如果你需要一份当时那一刻的快照就套上list()keys_snapshot list(info.keys())3.3 按分数排序输出遍历和排序的组合初学者常遇到的需求是把字典按值排序输出。比如把成绩从高到低列出来scores {alice: 85, bob: 92, carol: 78, dave: 88} for name in sorted(scores, keyscores.get, reverseTrue): print(name, scores[name]) # 输出 # bob 92 # dave 88 # alice 85 # carol 78sorted(scores, keyscores.get)的意思是对字典的键排序排序时的依据是每个键在字典中对应的值。reverseTrue从大到小。这个写法看起来短但背后逻辑得想清楚scores本身就是可迭代的迭代出来的是键key参数指定了比较规则。如果你想按键的字母序直接排序那更简单for name in sorted(scores): print(name, scores[name])3.4 遍历时修改字典一个臭名昭著的坑在遍历字典的过程中直接添加或删除键会立刻引发RuntimeError: dictionary changed size during iteration。举一个很典型的场景——删掉所有不及格的人scores {alice: 85, bob: 59, carol: 78, dave: 40} for name in scores: if scores[name] 60: del scores[name] # RuntimeError: dictionary changed size during iteration正确的做法是先取出所有需要删除的键存成一个列表然后再遍历列表去删字典for name in [k for k in scores if scores[k] 60]: del scores[name]列表推导式先算出不及格名单此时字典没有被修改是安全操作随后删除发生在循环外字典大小只在删的那一瞬间变化而循环已经不再遍历它了。4. get、setdefault、defaultdict告别手写键是否存在的判断在真实代码里最常见的场景并不是简单的存和取而是取一个值如果不存在就初始化然后立刻做运算。经典例子是词频统计统计一句话里每个单词出现了几次。4.1 最朴素的写法if 判断先把最笨的写法摆出来text apple banana cherry apple banana apple words text.split() count {} for word in words: if word in count: count[word] 1 else: count[word] 1 print(count) # {apple: 3, banana: 2, cherry: 1}这个版本逻辑完全正确但每次都写四行 if-else很啰嗦。字典的get可以把这段压缩成一行for word in words: count[word] count.get(word, 0) 1count.get(word, 0)的语义是有这个键就返回原值没有就返回 0。加上 1 后重新赋值。这一行代码是初学者理解字典聪明用法的第一步阶梯。4.2 setdefault先设默认值再干活比get更进一步的是setdefault它的行为是如果键不存在就把键和默认值插入字典然后返回值如果键存在就直接返回原值。用场景拆解一下group {} name bob group.setdefault(name, []).append(Python) group.setdefault(name, []).append(Django) print(group) # {bob: [Python, Django]}第一行执行时bob不存在于是插入bob: []再append(Python)得到{bob: [Python]}。第二行执行时bob已经存在直接返回原来的列表再append(Django)。这样你就不需要每次都检查键是否存在代码干净很多。4.3 defaultdict把默认值变成字典的固有属性同样是统计词频用collections.defaultdict还可以更进一步from collections import defaultdict count defaultdict(int) for word in words: count[word] 1 print(count) # defaultdict(class int, {apple: 3, banana: 2, cherry: 1})defaultdict(int)表示这个字典在访问不存在的键时会自动调用int()生成一个默认值 0。int()是零list()是空列表str()是空字符串。同理用它收集爱好列表hobbies defaultdict(list) for name, hobby in [(bob, Python), (alice, 游泳), (bob, Django)]: hobbies[name].append(hobby)注意defaultdict的副作用它会在你只是读取一个不存在的键时悄悄把这个键创建出来。比如print(count[tom])之后tom就真的存在于字典里了。如果你不想让读取产生副作用还是用普通字典加get更安全。4.4 再进一步Counter 一行统计词频词频统计太常见了标准库专门准备了一个Counterfrom collections import Counter count Counter(words) print(count) # Counter({apple: 3, banana: 2, cherry: 1}) print(count.most_common(2)) # [(apple, 3), (banana, 2)]Counter是字典的子类本质上就是一个值为计数的 dict但它多了most_common、elements等专用方法。新学字典时可以直接了解它因为它能帮你理解字典只是基础在其上可以扩展出很多专用结构。5. 字典嵌套学生信息、商品库存这类真实数据是这么组织的前面讲的都是一层字典但现实世界的数据很少是一个名字对应一个数这种简单关系。比如学生信息一个学生有姓名、年龄、多科成绩。如果用多个平行字典代码会变得极其难维护。字典的嵌套能力就是为此准备的。5.1 学生信息表value 里放字典把多个学生的信息放在一个字典里每个学生的信息内部又是一个字典这种结构叫字典的字典students { 001: { name: 张三, age: 18, scores: {math: 90, english: 85} }, 002: { name: 李四, age: 19, scores: {math: 76, english: 92} } }要取出 001 号学生的数学成绩就像剥洋葱一样一层一层往下走math_score students[001][scores][math] print(math_score) # 90遍历整个学生表时用items()解包外层再操作内层for stu_id, info in students.items(): print(stu_id, info[name], info[scores][math])这个例子的价值在于它让你看到字典的 value 完全可以是另一个字典嵌套的深度和你业务逻辑的层级一致。学生表有两层外层是学号内层是属性成绩又嵌套了一层。取数时几个方括号连续排列从左到右逐层深入逻辑清楚很多。5.2 商品库存一个更贴近生活的例子再做一个小型的商品库存系统stock { apple: {price: 5, quantity: 100}, banana: {price: 3, quantity: 200}, } # 卖出 20 个苹果 stock[apple][quantity] - 20 # 查看某个商品 def show(name): if name in stock: p stock[name][price] q stock[name][quantity] print(f{name}: 单价 {p} 元, 剩余 {q} 个) else: print(商品不存在) show(apple)这类嵌套结构远比两个平行列表清晰。把规律说透当同一个人的多个属性需要一起存储时就把它放在一个字典里当这些多个人又要按某个编号区分时再在外面套一层字典。数据结构的选择不是技巧问题而是真实世界关系的映射。5.3 嵌套太深时怎么办不过我也要给一个善意的提醒嵌套别超过三层。三层以上再想嵌套你写的时候倒是爽别人维护时读代码会很痛苦。遇到字典套字典再套字典的复杂结构更合理的做法是定义一个类或者至少抽象成带默认值的defaultdict以简化初始化。俗话讲字典是工具不是炫技场地。6. 列表、元组和字典互转zip、**展开与合并的常用姿势字典和序列类型之间经常要互相转换尤其是做数据清洗时。这一节把常用转换方法集中讲一遍属于那种平时不觉得一到用时恨不得抄下来的内容。6.1 列表、元组转字典最常见的两种方式一种是二元组列表配dict()pairs [(name, xiaoming), (age, 18)] d dict(pairs) print(d) # {name: xiaoming, age: 18}另一种是配合zip把两个列表合并成字典keys [name, age] values [xiaoming, 18] d dict(zip(keys, values)) print(d) # {name: xiaoming, age: 18}zip的作用是把两个序列的第 0 个元素组合成一对、第 1 个元素组合成一对产生一个二元组的可迭代对象正好可以被dict()消费。这是一种极其常见的并行列表转映射手段比手动循环优雅很多。6.2 字典转列表、元组反向转换的语义要记清楚d {name: xiaoming, age: 18} list(d.keys()) # [name, age] list(d.values()) # [xiaoming, 18] list(d.items()) # [(name, xiaoming), (age, 18)].items()转成的列表里每个元素是一个二元组。而list(d)直接得到的是键列表这是很多人没意识到的快捷方式。6.3 解包展开两个字典合并的多种姿势把字典展开成关键字参数可以用双星号**def show(name, age): print(name, age) info {name: xiaoming, age: 18} show(**info) # 等价于 show(namexiaoming, age18)用双星号还可以合并字典。三种常用写法如下d1 {a: 1, b: 2} d2 {b: 3, c: 4} # 写法一字典解包合并适合 Python 3.5 merged {**d1, **d2} # 写法二| 运算符Python 3.9 merged d1 | d2 # 写法三就地更新 d1.update(d2)三种方式的结果都是{a: 1, b: 3, c: 4}。注意b最终是 3因为后面的字典覆盖了前面的同名字段。update会直接修改原字典前两种则生成一个全新的字典。如果你不想动原来的数据优先用解包方式或者|运算符。6.4 fromkeys为多个键批量设置默认值还有一个看起来冷门但很实用的小工具d dict.fromkeys([a, b, c], 0) print(d) # {a: 0, b: 0, c: 0}dict.fromkeys接收一个键的列表和同一个默认值生成一个新字典。适合做初始化比如给一堆单词初始化计数值。需要提醒的是默认值如果传可变对象所有键会共享同一个对象例如dict.fromkeys([a, b], [])里的两个键都指向同一个空列表改一个看起来会影响另一个——这通常不是你想要的效果。这时最好改用字典推导式d {k: [] for k in [a, b]}7. 初学者最容易踩的五个坑每个都给你复现一遍我见过太多人在这个阶段卡住不是因为字典难而是因为踩了下面这类无声无息的坑。把这些坑提前认一遍能帮你省下大量调试时间。7.1 用列表当键直接 TypeError很多初学者想让字典记录某种列表状态就会尝试d {} d[[1, 2, 3]] test # TypeError: unhashable type: listPython 字典靠哈希表实现键必须是可哈希的而列表是可变类型它的长相会变哈希地址也会变作为键会导致存储位置不稳定。解决方案是改成元组d[(1, 2, 3)] test print(d) # {(1, 2, 3): test}这里也顺便解释了为什么元组可以当键、列表不行元组不可变它的哈希值固定字典内部才能稳定查找。7.2 重复键不报错而是静默覆盖看这个例子d {a: 1, a: 2} print(d) # {a: 2}不会报错但后面的值把前面的值覆盖了。这种坑经常出现在把两个配置字典合并时如果两个来源使用了相同的键你以为是追加实际上被后者覆盖。用{**d1, **d2}合并时尤其要注意键冲突的优先级。7.3 方括号取不到值直接让程序崩掉对字典做读取时d[不存在]会抛KeyError很多新手在写爬虫、处理接口返回数据时被这里搞崩过。解决思路很简单如果你不能百分百确认键存在就换d.get(不存在, 默认值)如果后续要对该键做运算需要考虑用setdefault或者defaultdict。经验法则读取不确定的键永远优先 get。7.4 字典复制改了一个另一个也变了这是经典中的经典a {list: [1, 2, 3]} b a b[list].append(4) print(a) # {list: [1, 2, 3, 4]}b a并没有复制字典只是让两个变量指向了同一个字典对象。如果你需要复制用copy()是浅拷贝b a.copy() b[list].append(4) print(a) # {list: [1, 2, 3, 4]}还是被改了因为浅拷贝复制的是最外层内部的可变对象仍然共享。要真正独立必须用深拷贝import copy b copy.deepcopy(a) b[list].append(4) print(a) # {list: [1, 2, 3]} print(b) # {list: [1, 2, 3, 4]}理解浅拷贝和深拷贝的差异是字典实用道路上的必修课很多 bug 排查半天最后都发生在这一层。7.5 对字典顺序的误解Python 3.7 之后字典正式保持插入顺序。也就是说info {} info[b] 2 info[a] 1 print(list(info)) # [b, a]先插入的键排在前面。但插入有序不等于自动排序。如果你想按字母序或者按值的大小输出还是要显式调用sorted不要指望插入顺序能满足排序需求。比如构造字典时故意按某种业务顺序插入能增强可读性但如果你需要按值排序请继续使用前面讲的sorted(scores, keyscores.get)。7.6 顺手补充读写时注意 bool 的坑最后补一个进阶一点的小坑True和1在字典里被视为同一个键。d {} d[True] yes d[1] no print(d) # {True: no}因为True 1且哈希值相同所以第二次赋值把第一次覆盖了。类似地False和0也会互相覆盖。这种问题在实际业务中不常见但一旦出现会很难排查知道有这么回事就够了。我用这套方法带过好几届新手最有效的练习始终是同一个把你手机里的通讯录、钱包里的账单、游戏里的几个角色属性用字典重新实现一遍。不用背 API写几个小例子这些操作自然就长在记忆里了。如果让我给初学者留一句话字典最大的价值就是按名字找值所有方法都围绕这个核心展开遇到任何数据结构需求先问自己一句——这到底是一个名字对应一个值还是一个顺序对应一个位置想清楚这个问题你的数据建模能力就算正式入门了。
企业数字化 ERP 产品动态
相关推荐
SimpleMES加工装配系统:工单追溯与返修闭环的轻量级落地实践 简介:这是一套面向制造业信息化开发者与MES学习者的加工装配模拟系统源码资料,基于Visual Studio 2010与SQLServer2008R2、.NET 4.0开发,适合希望理解MES核心业务逻辑与实现方式的中级开发者参考。系统分为服务端与客户端两大部分:… · 2026/9/26 7:33:05
二手测头能不能买?评估要点、风险识别与决策指南 二手测头能不能买?评估要点、风险识别与决策指南数据来源说明:本文为二手机床测头(工件测头、对刀仪)采购决策的评估方法与风险识别指南,旨在帮助工厂在预算有限或机型停产等场景下,对二手件做出基于证据的… · 2026/9/26 7:32:59
18650锂电池热效应建模指南:从集总参数到三维仿真 1. 为什么偏偏是18650:先搞清热从哪来、往哪走18650这个名字,资历老一点的电池工程师闭着眼都知道——直径18毫米、长度65毫米的圆柱电芯。从特斯拉早期Model S用的松下NCR18650B,到如今电动工具、储能模组、笔记本电池乃至各类DIY项目&#… · 2026/9/26 8:08:26
用.NET MAUI打造零信任套壳浏览器:跨平台企业访问控制实战 直接开工。公司内部业务系统越来越多,Web端应用占了绝大多数,但员工既要访问内网系统,又要处理外部站点,浏览器里混杂着各种书签和Cookie,安全部门每次做访问控制审计都焦头烂额。我当时的想法很简单:能不能… · 2026/9/26 8:08:26
Vue3 开发提效:在 VSCode 插件里配 TaoToken 的 settings.json 骨架 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 8:08:26
开源离线音频格式转换器:23种格式无损转换与音质保障 1. 项目概述:为什么一个“永不收费”的音频转换器值得花20分钟读完你有没有过这样的经历:剪辑视频时突然发现配音文件是 .aac 格式,而剪辑软件只认 .wav;或者从老设备导出了一堆 .amr 语音备忘录,想发给同事却被告知“… · 2026/9/26 8:08:26
Claude Code 200K上下文的正确用法:上下文工程实战指南 1. 这不是“上下文越大越好”的简单问题,而是你根本没搞懂“上下文”在AI里到底在干什么 “Claude Code 的 200K 上下文,为什么救不了你的 AI?”——这个标题一出来,很多刚接触大模型的朋友第一反应是:200K࿱… · 2026/9/26 8:08:20
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46