拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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张三, age28) # 传入可迭代的键值对序列 d2 dict([(name, 张三), (age, 28)]) # 传入另一个字典浅拷贝 d3 dict(d2) # 关键字和可迭代混用 d4 dict({name: 张三}, age28)关键字参数形式写起来清爽但键只能是字符串且必须是合法Python标识符像dict(1a)或dict(my-keya)都会报语法错误。所以如果你的键是数字或者带特殊字符的字符串只能用可迭代序列那种形式。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(kv)简洁键值有规律可计算推导式快且紧凑批量占位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) # 返回0d变成{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(f6, g7) # 传关键字参数关键行为已存在的键会被覆盖不存在的键会新增。这个覆盖特性在配置合并场景里特别有用——默认配置打底用户配置覆盖上去。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) # 返回1d变成{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 # Truein判断的是键不是值。要判断值在不在得用in d.values()但那是O(n)的线性查找和字典的O(1)键查找完全不是一个量级。5.3 三种取值方式的性能对比我实测了100万次查询字典有10万个键方式耗时说明d[k]约0.05秒最快无函数调用d.get(k)约0.08秒多一次方法调用k in d再d[k]约0.10秒两次哈希查找差距看着不大但在热点循环里累积起来就明显了。能确定键存在就用方括号这是最快的。5.4 嵌套字典的安全查询多层嵌套的字典直接链式取值很容易在某层断掉data {user: {profile: {name: 张三}}} # data[user][profile][age] # KeyError安全写法有两种。一是逐层getage data.get(user, {}).get(profile, {}).get(age, 0)二是用try/excepttry: 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 d和for 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(), keylambda x: x[1]): print(k, v) # 按值降序 for k, v in sorted(d.items(), keylambda x: x[1], reverseTrue): 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 1print([type(k) for k in d])从JSON读的键查不到JSON键都是字符串确认代码里用的是字符串中文键报错编码问题确认文件编码和字符串编码一致嵌套字典报错中间层不存在逐层get或try7.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更省内存有序键值对dict3.7或OrderedDict插入序计数统计collections.Counter专门优化一键多值defaultdict(list)自动初始化只读映射types.MappingProxyType防止误改8. 把字典用顺手之后的一些体会字典这东西语法层面半小时就能学完但真正用顺手得靠项目里反复磨。我自己的转折点是开始有意识地关注这次操作用了几次哈希查找——d[k]一次d.get(k)一次k in d加d[k]两次for k in d: d[k]每次循环两次。把这些数清楚之后代码自然就快了。还有一个习惯是建字典之前先想清楚键是什么类型。字符串键最通用数字键省内存元组键能表达复合条件。但不管选哪种整个字典里保持一致别混着来。最后分享一个我常用的调试技巧怀疑字典有问题时先print(len(d))看大小对不对再print(list(d.items())[:5])看前几条数据长什么样最后print([type(k) for k in list(d)[:5]])看键的类型。这三步下来九成的字典问题都能定位。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门