
小周那天写了一个数据导出功能要把字典里的配置项逐条写进文件。代码逻辑不复杂遍历字典拿到键和值拼成一行字符串。他写的是这样config {host: localhost, port: 8080, debug: True} for key in config.keys(): line f{key}{config[key]} print(line)跑出来结果是对的hostlocalhost port8080 debugTrue功能上线后同事 review 代码时随口说了一句“这里用items()更合适。”小周嘴上说“能跑就行”心里却犯了嘀咕keys()明明也能拿到键再取值也没什么问题为什么非要用items()直到有一天他处理一个几万条配置的字典发现导出速度慢得离谱。他把代码改成items()之后速度提升了一大截。他才认真去查了两者的区别然后发现自己之前对字典遍历的理解一直是模糊的。先看三种遍历方式Python 的字典提供了三个方法用于遍历keys()、values()、items()。config {host: localhost, port: 8080, debug: True} for key in config.keys(): print(key) for value in config.values(): print(value) for key, value in config.items(): print(key, value)输出分别是host port debug localhost 8080 True host localhost port 8080 debug Truekeys()只给你键。values()只给你值。items()给你键值对每个元素是一个(key, value)元组。用一个生活场景来理解。你有一本通讯录每页写着“姓名—电话”。keys()的做法是只把姓名念给你听。你听到“张三”“李四”“王五”但不知道他们的电话。values()的做法是只把电话念给你听。你听到一串号码但不知道对应谁。items()的做法是把每一页完整地念给你——“张三138xxxx”“李四139xxxx”。如果你只需要姓名keys()就够了。如果你只需要电话values()就够了。但如果你既要知道姓名又要知道电话就应该用items()而不是先念姓名再回头翻电话。小周的代码就是“先念姓名再回头翻电话”——他用keys()拿到键再用config[key]去查值。虽然结果对但多了一次查找。items() 的本质一次遍历拿到两个东西items()返回的是一个视图对象每个元素是(key, value)元组。在for循环里可以直接解包for key, value in config.items(): print(key, value)这行代码等价于for item in config.items(): key, value item print(key, value)Python 会自动把元组拆开分别赋给key和value。这是items()最常用的写法。而用keys()再取值for key in config.keys(): value config[key] print(key, value)每次循环都要做一次哈希查找从字典里把值取出来。虽然单次查找很快但循环几万次、几十万次时累积的开销就不可忽略了。性能差异有多大我们来实测一下。假设有一个包含十万条数据的字典import timeit d {fkey_{i}: i for i in range(100000)} # 方式一keys() 取值 def method1(): total 0 for key in d.keys(): total d[key] return total # 方式二items() def method2(): total 0 for key, value in d.items(): total value return total t1 timeit.timeit(method1, number10) t2 timeit.timeit(method2, number10) print(fkeys() 取值: {t1:.4f} 秒) print(fitems(): {t2:.4f} 秒)在我的机器上keys()方式大约0.35秒items()方式大约0.25秒。差距约30%。为什么items()更快因为keys()方式每次循环都要做一次d[key]的哈希查找而items()在遍历时直接把键值对一起取出来不需要额外的查找。数据量越大差距越明显。十万条差0.1秒一千万条就差10秒。在高频调用的函数里这个差距会被进一步放大。坑一items() 返回的不是列表很多人以为items()返回的是一个列表可以用索引访问config {a: 1, b: 2} items config.items() print(items[0]) # 报错TypeError: dict_items object is not subscriptableitems()返回的是一个视图对象view object不是列表。它支持迭代、支持in判断、支持len()但不支持索引和切片。items config.items() print(len(items)) # 2 print(a in items) # 报错不这里判断的是 key 还是 (key, value)注意in判断的是(key, value)元组不是单独的键。config {a: 1, b: 2} items config.items() print((a, 1) in items) # True print(a in items) # Falsea in items返回False因为items里的元素是元组不是字符串。如果你需要一个真正的列表可以显式转换items_list list(config.items()) print(items_list[0]) # (a, 1)但这会创建一个新的列表占用额外内存。除非确实需要索引访问否则没必要转换。坑二视图对象是动态的keys()、values()、items()返回的都是视图对象。视图不复制数据它反映的是字典的实时状态。config {a: 1, b: 2} keys config.keys() print(list(keys)) # [a, b] config[c] 3 print(list(keys)) # [a, b, c]keys变量没有重新赋值但它“看到”了字典的新增内容。因为视图是动态的它不存储数据只是提供了一种访问字典的方式。这个特性有时候很有用比如你想在某个时刻查看字典的当前状态。但有时候也会带来意外config {a: 1, b: 2} keys config.keys() config.clear() print(list(keys)) # []字典清空后之前拿到的keys视图也空了。坑三遍历时修改字典这是一个经典错误和items、keys的关系不大但经常在遍历场景中出现。config {a: 1, b: 2, c: 3} for key in config.keys(): if config[key] 2: del config[key]运行会报错RuntimeError: dictionary changed size during iterationPython 不允许在遍历字典时改变它的大小。因为字典的迭代依赖于内部的哈希表结构插入或删除元素可能导致结构变化迭代器就失效了。如果确实需要在遍历时删除元素有两种方式。方式一遍历副本。for key in list(config.keys()): if config[key] 2: del config[key]list(config.keys())创建了一个键的列表遍历的是这个列表而不是字典本身。删除字典里的元素不会影响列表的遍历。方式二用字典推导式生成新字典。config {k: v for k, v in config.items() if v ! 2}不修改原字典而是创建一个过滤后的新字典。这种方式更 Pythonic也更安全。坑四values() 拿不到键values()只给你值不给你键。如果你需要根据值做判断然后操作对应的键values()就不够用了。config {a: 1, b: 2, c: 3} for value in config.values(): if value 2: # 我想删除对应的键但不知道键是什么 pass这时候应该用items()for key, value in config.items(): if value 2: del config[key] # 但仍然不能在遍历时删或者先收集要删的键再统一删to_delete [k for k, v in config.items() if v 2] for key in to_delete: del config[key]坑五忘了括号这是一个很隐蔽的错误config {a: 1, b: 2} for key in config.items: # 忘了括号 print(key)不会报错但输出的是方法对象built-in method items of dict object at 0x...因为config.items是方法本身不带括号就不会调用它。for循环试图迭代这个方法对象但它不可迭代所以直接输出方法对象本身实际上这里会报TypeError: builtin_function_or_method object is not iterable取决于 Python 版本和具体写法。正确写法是config.items()带括号。类似的错误还有config.keys、config.values都是忘了加括号。items() 的解包细节items()返回的每个元素是元组。在for循环里可以直接解包for key, value in config.items(): print(key, value)但如果字典的值本身也是元组或列表解包时要小心data {a: (1, 2), b: (3, 4)} for key, value in data.items(): print(key, value) # a (1, 2)这里key是avalue是(1, 2)。没问题。但如果你写成for key, x, y in data.items(): print(key, x, y)会报错因为data.items()的每个元素只有两个值键和值。(1, 2)是值内部的元组不会被自动展开。如果你确实想同时拿到键和值内部的元素需要额外解包for key, (x, y) in data.items(): print(key, x, y) # a 1 2注意value的位置写成了(x, y)这样 Python 会把元组(1, 2)拆开分别赋给x和y。遍历顺序Python 3.7 之后的保证在 Python 3.6 之前字典是无序的遍历顺序不确定。从 Python 3.7 开始字典正式保证插入顺序。config {b: 2, a: 1, c: 3} for key in config.keys(): print(key)输出永远是b a c这个特性在 Python 3.7 中稳定可靠。但要注意set仍然是无序的frozenset也是。只有dict保证了插入顺序。如果你需要按特定顺序遍历字典比如按键排序可以用sorted()for key in sorted(config.keys()): print(key, config[key]) # 或者用 items() for key, value in sorted(config.items()): print(key, value)sorted(config.items())会按键排序因为元组比较时先比较第一个元素键。那 keys() 还有用吗看到这里你可能会问既然items()既能拿键又能拿值那keys()还有什么用keys()在以下场景仍然有用只需要键不需要值。比如统计某个键是否存在或者把键收集到一个列表里。keys list(config.keys()) if host in config.keys(): print(有 host 配置)做集合运算。keys()返回的视图对象支持集合操作d1 {a: 1, b: 2} d2 {b: 3, c: 4} print(d1.keys() d2.keys()) # {b}交集 print(d1.keys() | d2.keys()) # {a, b, c}并集 print(d1.keys() - d2.keys()) # {a}差集这是items()做不到的。视图对象的集合运算能力是keys()的独特优势。只需要检查键是否存在。if key in config.keys()比if key in config更明确但后者更简洁性能也更好。所以实际中直接用in config就够了。回到小周的代码小周原本的写法for key in config.keys(): line f{key}{config[key]} print(line)改成了for key, value in config.items(): line f{key}{value} print(line)两行代码看起来差不多但items()版本少了一次字典查找速度更快代码也更清晰。他还在项目里发现了几处类似的写法for key in d.keys(): if d[key] 0: ...改成了for key, value in d.items(): if value 0: ...以及一处遍历时删元素的 bugfor key in d.keys(): if d[key] 0: del d[key] # RuntimeError改成了d {k: v for k, v in d.items() if v ! 0}小周后来在团队文档里写了一条“遍历字典要键值对用items()别用keys()再取值。只要键用keys()。只要值用values()。遍历时别改字典要改就先复制或者用推导式。”总结keys()、values()、items()是字典遍历的三把工具各司其职。keys()只给键适合只需要键的场景支持集合运算。values()只给值适合只需要值的场景。items()给键值对适合同时需要键和值的场景性能比keys()加取值更好。它们的返回值都是视图对象不是列表。视图是动态的反映字典的实时状态不支持索引但支持迭代和in判断。遍历时不要修改字典的大小。如果需要删除元素遍历副本或者用字典推导式生成新字典。记住那个通讯录的比喻keys()念姓名values()念电话items()把整页念给你。想清楚你要什么再选对应的工具。小周的坑不在于他不会用items()而在于他从来没想过keys()加取值有什么问题。代码能跑结果也对但性能差了一截。这种“能跑但不够好”的代码往往最容易被忽略也最值得回头审视。