前言
很多人从 C 或 Java 转过来,会先入为主地把for理解成「计数器循环」——初始化、判断、自增三段式。Python 的for完全不是这个模型。它的语义是遍历一个可迭代对象(iterable),把里面的元素一个一个取出来。range(10)看起来很像个计数器,其实它只是"可迭代对象"里最常见的一种。
这个理解差异会带来一批连锁误解:以为for循环变量有独立的块级作用域(其实泄漏到外层)、以为for一定要写range(len(x))(其实直接遍历元素更清晰)、以为range(1, 5)会包含 5(它是左闭右开)。
本文用一组实战案例把for的常见用法铺开:带序号遍历、同时遍历多个序列、遍历字典、for ... else做查找、嵌套循环里的break、以及反转与排序遍历。每个例子都能直接复制运行。
顺便说一句 Python 2:xrange这个名字在 Python 3 里已经被移除,因为range本身就是惰性的——它返回的是一个按需产生整数的对象,不会像 Python 2 的range那样一次性生成整个列表。同理,dict.iteritems()/iterkeys()/itervalues()也都没了,一律用.items()/.keys()/.values(),它们返回的是视图(view)而不再是列表。
一、for到底在遍历什么
for x in obj的执行过程只有两步:先调用iter(obj)拿到一个迭代器(iterator),然后反复对这个迭代器调用next(),直到抛出StopIteration。所以任何实现了__iter__的对象都能被for遍历——列表、元组、字符串、字典、集合、文件对象、生成器、range对象,全都是。
理解这一点,就能理解下面两件事:
- 生成器只能遍历一次。第一次
for把它消耗完了,第二次for什么都拿不到,因为它本身既是可迭代对象又是迭代器。 - 遍历字典拿到的是键(key),不是键值对。要键值对得显式写
.items()。
| 你想要的 | 写法 | 返回什么(Python 3) |
|---|
| 遍历元素 | for x in seq | 元素本身 |
| 带序号 | for i, x in enumerate(seq) | 序号 + 元素 |
| 并行遍历多个 | for a, b in zip(s1, s2) | 各序列对应位置 |
| 遍历字典键 | for k in d | 键 |
| 遍历键值对 | for k, v in d.items() | 键、值 |
| 倒序遍历 | for x in reversed(seq) | 反序元素 |
| 排序后遍历 | for x in sorted(seq, key=...) | 排序后的元素 |
| 数字区间 | for i in range(a, b) | 整数,左闭右开 |
二、实战案例
案例一:带序号遍历
# 适用于 Python 3.8+
names = ["alice", "bob", "carol"]
for index, name in enumerate(names, start=1):
print(index, name)
# 1 alice / 2 bob / 3 carolenumerate(iterable, start=0)的第二个参数是起点,默认从 0 开始。不要写for i in range(len(names)): name = names[i],那种写法既啰嗦又容易写错下标。
案例二:同时遍历两个序列
# 适用于 Python 3.8+
skus = ["A001", "A002", "A003"]
prices = [399, 129, 59]
for sku, price in zip(skus, prices):
print(sku, "=", price)zip会在最短的序列耗尽时停止。也就是说skus有三个元素而prices只有两个时,第三个 sku 会被静默丢掉。如果这属于数据错误、你希望它报错,Python 3.10 起可以写zip(skus, prices, strict=True),长度不一致会抛ValueError。
案例三:遍历字典
# 适用于 Python 3.8+
# 3.7 起字典保证按插入顺序遍历
scores = {"math": 90, "english": 85, "history": 78}
for key in scores: # 等价于 scores.keys()
print("科目:", key)
for value in scores.values():
print("分数:", value)
for subject, score in scores.items():
print(subject, "->", score).keys()、.values()、.items()返回的都是视图,它们不是列表,会随字典内容动态变化。想固定下来要显式list(scores.items())。
案例四:for ... else做查找
# 适用于 Python 3.8+
names = ["alice", "bob", "carol"]
target = "dave"
for name in names:
if name == target:
print("找到了:", name)
break
else:
print("列表中没有", target)else子句只在循环没有被break中断时执行。它天生适合表达"找遍了都没有"这种语义,比设一个found标志位再判断要干净。注意:如果循环体里有return,else也可能被执行——它是"没有 break",不是"没有提前退出"。
案例五:嵌套循环里的break
# 适用于 Python 3.8+
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
position = None
for row_index, row in enumerate(matrix):
for col_index, value in enumerate(row):
if value == 5:
position = (row_index, col_index)
break # 只跳出内层循环
if position is not None:
break # 外层需要再判断一次
print("5 的位置:", position) # (1, 1)break只作用于它所在的那一层循环。要一次跳出多层,标准写法是把逻辑抽成函数用return直接返回,或者用for ... else配合标志位。Python 没有break 2这种带层数的语法。
案例六:反转、排序与区间
# 适用于 Python 3.8+
for ch in reversed("abc"):
print(ch) # c b a
for value in sorted([3, 1, 2], reverse=True):
print(value) # 3 2 1
for i in range(10, 0, -3):
print(i) # 10 7 4 1reversed()要求对象要么是序列,要么实现了__reversed__。它不能反转任意迭代器,比如把一个生成器传进去会直接TypeError。sorted()返回新列表,原序列不动;如果原对象是列表且你想原地排序,用lst.sort()——注意它返回None,别写for x in lst.sort()。
常见坑点
1. 遍历列表的同时增删元素
❌
for item in nums:
if item % 2 == 0:
nums.remove(item) # 索引在变,会漏掉元素✅
nums = [x for x in nums if x % 2 != 0]一边走一边动,迭代器内部的索引和实际列表错位,结果是"删了一些但没删干净",且不报错。
2. 遍历字典时改它的大小
❌for k in d: d["new_" + k] = 1→RuntimeError: dictionary changed size during iteration✅for k in list(d): d["new_" + k] = 1先转成列表把键拷出来,再改原字典。改值(不改键集合)是允许的。
3. 以为range(1, 5)包含 5
❌list(range(1, 5))期待得到[1, 2, 3, 4, 5]✅ 它是左闭右开,实际是[1, 2, 3, 4],要含 5 得写range(1, 6)
4. 用range(len(x))取元素
❌
for i in range(len(names)):
print(names[i])✅for name in names: print(name)只有确实需要下标时才用enumerate。range(len(...))既慢又容易越界。
5. 以为for循环变量只在循环内有效
❌ 在外层用同名变量,以为不会被覆盖 ✅ 要知道for不会创建新作用域,循环结束后变量保留最后一次的值
for i in range(3):
pass
print(i) # 2,i 仍然存在6. 在闭包里引用循环变量
❌
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2],全是最后一个值✅
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]这是延迟绑定:闭包捕获的是变量本身,不是当时的值。
7. 用reversed()反转生成器
❌reversed(x for x in range(3))→TypeError: 'generator' object is not reversible✅ 先物化:reversed(list(x for x in range(3))),或用sorted(..., reverse=True)
8. 忽略zip的截断行为
❌ 拿两个长度不同的列表zip后直接当成一一对应 ✅ Python 3.10+ 用zip(a, b, strict=True)主动暴露长度不一致 静默截断会让"少的那部分数据"消失得无声无息。
总结
| 需求 | 推荐写法 | 要避开的写法 |
|---|
| 带序号 | enumerate(seq, start=1) | range(len(seq)) |
| 并行遍历 | zip(a, b) | 用下标同时访问两个列表 |
| 键值对 | d.items() | d.iteritems()(Python 2,已移除) |
| 找不着就报错 | for...else | 手动设标志位 |
| 跳多层循环 | 抽成函数用return | 层层设标志位 |
| 过滤重建 | 列表推导式 | 边遍历边remove |
for在 Python 里是"遍历"而不是"计数",接受这个心智模型之后,绝大多数写法问题都会自然消失:不再需要下标、不再需要手写标志位、不再需要担心越界。真正需要盯住的,只剩下"不要在遍历过程中改变被遍历的对象"这一条。