Python的字典遍历把我坑惨了,原来items和keys的区别这么大

简介: 本文通过小周优化字典遍历的真实案例,深入解析Python中`keys()`、`values()`、`items()`三大遍历方法的本质区别与适用场景。重点揭示`items()`一次获取键值对的性能优势(十万数据快30%),并详解五大常见误区:视图非列表、动态性、遍历时修改字典、`values()`无键、忘加括号等,助你写出更高效、更安全的Python代码。(239字)

小周那天写了一个数据导出功能,要把字典里的配置项逐条写进文件。代码逻辑不复杂:遍历字典,拿到键和值,拼成一行字符串。

他写的是这样:

config = {"host": "localhost", "port": 8080, "debug": True}

for key in config.keys():
   line = f"{key}={config[key]}"
   print(line)

跑出来结果是对的:

host=localhost
port=8080
debug=True

功能上线后,同事 review 代码时随口说了一句:“这里用items()更合适。”小周嘴上说“能跑就行”,心里却犯了嘀咕:keys()明明也能拿到键,再取值也没什么问题,为什么非要用items()?

直到有一天他处理一个几万条配置的字典,发现导出速度慢得离谱。他把代码改成items()之后,速度提升了一大截。他才认真去查了两者的区别,然后发现自己之前对字典遍历的理解一直是模糊的。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (99).png

先看三种遍历方式

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 True

keys()只给你键。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 = {f"key_{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, number=10)
t2 = timeit.timeit(method2, number=10)

print(f"keys() + 取值: {t1:.4f} 秒")
print(f"items(): {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 subscriptable

items()返回的是一个视图对象(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)       # False

"a" 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 iteration

Python 不允许在遍历字典时改变它的大小。因为字典的迭代依赖于内部的哈希表结构,插入或删除元素可能导致结构变化,迭代器就失效了。

如果确实需要在遍历时删除元素,有两种方式。

方式一:遍历副本。

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是"a",value是(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:
       ...

以及一处遍历时删元素的 bug:

for 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()加取值有什么问题。代码能跑,结果也对,但性能差了一截。这种“能跑但不够好”的代码,往往最容易被忽略,也最值得回头审视。

目录
相关文章
|
Linux 编译器 程序员
【Linux 调试秘籍】深入探索 C++:运行时获取堆栈信息和源代码行数的终极指南
【Linux 调试秘籍】深入探索 C++:运行时获取堆栈信息和源代码行数的终极指南
1699 0
|
1天前
|
人工智能 安全 小程序
阿里云服务器199元1年购买与使用常见问题:2核4G5M带宽,续费同价,企业用户专享
阿里云199元/年通用算力型u1实例(ecs.u1-c1m2.large)面向企业实名认证用户,配置2核4G、5Mbps固定公网带宽、80GB ESSD Entry云盘,主打"续费同价",活动延至2029年3月31日,最长可同价使用5年,适合建站、开发测试、后台管理、小程序及跨境出海等通用场景;个人账号无法购买,同一企业主体最多保有一台,适合预算敏感的中小企业长期选用。
|
1月前
|
人工智能 自然语言处理 API
阿里云百炼Token Plan和Coding Plan有什么区别?CodingPlan在哪购买?个人开发者选哪个省钱?
阿里云百炼两大订阅:Token Plan按Credits计费,支持多模态模型及各类AI工具;Coding Plan为固定月费¥200(限时首月¥39.9),仅限交互式编程,限量抢购。官方推荐Token Plan,适用更广、灵活性更强。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
|
5月前
|
人工智能 运维 Unix
Linux 的 arch 命令
`arch`是Linux下查看硬件架构的简洁命令,等价于`uname -m`,输出如`x86_64`、`aarch64`等标识符,广泛用于脚本判断、软件安装及系统运维,属coreutils包,无需参数即可快速获取CPU架构类型。(239字)
|
9月前
|
前端开发 CDN
微信小游戏的开发费用
微信小游戏开发费用因美术、玩法等差异大,轻量级3-8万,精品中度10-35万,重度游戏达50万以上。另需考虑软著、版号、服务器及买量等硬成本,运营投入常超开发。2026年混合变现与首发激励成新趋势。#微信游戏 #游戏外包
|
8月前
|
人工智能
从"任天堂仿作"到动态封禁:生成式游戏的内容审核难题
The Verge测评揭示Project Genie在模仿经典游戏风格时的版权困境:关键词过滤易被绕过,输出审核难定“相似度”,人工审核不可持续。问题核心在于AI能否合法模仿创作风格——法律尚未明确,技术尚不成熟。平台需透明规则、分层发布与版权方协作。
378 0
|
11月前
|
机器学习/深度学习 人工智能 算法
乘AIGC浪潮:把握万亿级机遇
AIGC正加速从技术走向产业落地,万亿市场规模催生全链条人才需求。北京、上海政策加码,算力基建完善,2025-2027年成关键窗口期。七大核心岗位——AIGC工程师、大模型训练师、AI工程师等全面爆发,覆盖技术到应用各层级,高薪抢人成常态。工信部认证加持,职业前景广阔,人人皆可入局,抢占AI时代新风口。
909 1
|
安全 搜索推荐 开发者
【适度精简】Windows 7 旗舰版-emmy精简系统
Windows 7旗舰版因硬件占用高、冗余组件多、兼容性差及缺乏安全更新等问题,逐渐难以满足用户需求。适度精简版通过去除无用组件、优化性能与安全性,提升老旧设备运行效率,增强兼容性与稳定性,同时保留用户熟悉的操作界面,降低学习成本,满足个性化需求,延续Windows 7的实用价值。
610 2
|
存储 Java Maven
SpringBoot整合Jest和Elasticsearch实践
SpringBoot整合Jest和Elasticsearch实践
738 1
|
人工智能 缓存 搜索推荐
百度/Bing/Google搜索引擎使用技巧
本文分享了百度、Bing和Google三大搜索引擎的实用技巧,涵盖精确匹配、排除关键词、站内及文件类型搜索等,如使用双引号进行精确搜索“人工智能应用”,排除特定词如“人工智能 -游戏”,以及在特定网站如“site:baidu.com 人工智能”内查找内容等,帮助提高搜索效率和准确性。
2057 8
百度/Bing/Google搜索引擎使用技巧