迭代
迭代就是按照一定顺序,一次处理一个元素。例如,逐个读取列表中的姓名、逐行处理文件,或者依次访问字典中的键,都是迭代。
Python 最常见的迭代工具是 for 循环。它不要求对象一定是列表,而是会尝试从对象中取得一个迭代器,再不断取出下一个值。因此,理解迭代是理解 for、生成器和迭代器的基础。
本文示例适用于 Python 3。学完后,你应该能够:
- 使用
for、enumerate()和zip()编写清晰的逐项处理逻辑; - 判断一个对象是否可以被迭代;
- 理解
for循环如何通过iter()和next()工作; - 正确处理
break、continue、else和循环中的解包; - 识别迭代期间修改容器、重复消费数据等常见问题。
从 for 循环开始
colors = ["red", "green", "blue"]
for color in colors:
print(color)
循环会依次把 colors 中的元素绑定到变量 color,循环体每次执行一次。循环变量不是特殊语法,它只是当前元素的一个名字:
total = 0
for price in [12, 8, 15]:
total += price
print(total) # 35
for 适合表达“对每个元素做同一件事”。如果只需要重复执行固定次数,可以迭代 range():
for number in range(3):
print(number) # 0、1、2
range(3) 表示从 0 开始,到 3 之前结束,和切片一样采用左闭右开规则。它是一个可迭代对象,不会预先创建一个包含所有整数的列表。
什么是可迭代对象
能够被 for 循环逐项读取的对象,称为可迭代对象(iterable)。常见例子包括:
for value in [1, 2, 3]: # list
pass
for char in "Python": # str
pass
for key in {"name": "Ada"}: # dict,默认迭代键
pass
for number in range(3): # range
pass
元组、集合、字典视图、文件对象以及许多第三方对象也可以迭代。字典默认迭代键;需要同时处理键和值时,应明确调用 items():
scores = {"Ada": 95, "Lin": 88}
for name, score in scores.items():
print(f"{name}: {score}")
for 循环的工作过程
下面的代码展示了 for 的核心机制:
values = [10, 20, 30]
iterator = iter(values)
while True:
try:
value = next(iterator)
except StopIteration:
break
print(value)
for value in values 大致等价于以下步骤:
- 调用
iter(values)得到迭代器; - 重复调用
next(iterator)获取下一个值; - 迭代器没有更多值时抛出
StopIteration; for捕获这个异常并正常结束循环。
StopIteration 是迭代协议的控制信号,不表示程序出错。通常不需要手动捕获它,直接使用 for 即可。迭代器的详细实现方式见“迭代器”主题。
循环中的解包
如果每次得到的是元组或列表,可以在循环头部直接解包:
points = [(2, 3), (5, 8)]
for x, y in points:
print(x + y)
元素结构不符合预期时,解包会抛出 ValueError,这通常能尽早发现数据问题。需要保留剩余项时,可以使用带星号的解包:
rows = [("Ada", 95, "A"), ("Lin", 88, "B")]
for name, *details in rows:
print(name, details)
同时取得索引:enumerate()
不要为了取得索引而手动维护计数器:
tasks = ["read", "write", "test"]
for index, task in enumerate(tasks, start=1):
print(f"{index}. {task}")
enumerate() 返回一个迭代器,每次产生 (索引, 元素)。start 只改变显示的起始数字,不会修改原序列。
并行遍历:zip()
zip() 可以把多个可迭代对象按位置配对:
names = ["Ada", "Lin", "Mo"]
scores = [95, 88, 91]
for name, score in zip(names, scores):
print(name, score)
默认情况下,zip() 在最短对象耗尽时停止:
print(list(zip([1, 2, 3], ["a", "b"])))
# [(1, 'a'), (2, 'b')]
如果长度必须相同,可以在 Python 3.10 及以上使用 strict=True,长度不一致时立即抛出 ValueError:
for name, score in zip(names, scores, strict=True):
pass
较早版本可以先检查长度,或者使用 itertools.zip_longest() 为较短对象填充默认值。zip() 本身是一次性的迭代器,调试时不要忘记这一点。
控制循环流程
continue 跳过本轮
for number in range(6):
if number % 2 == 0:
continue
print(number) # 1、3、5
break 提前结束
for name in ["Ada", "Lin", "Mo"]:
if name == "Lin":
break
print(name) # Ada
for ... else
循环的 else 会在循环自然结束时执行;如果循环通过 break 结束,则跳过 else:
def find_even(numbers):
for number in numbers:
if number % 2 == 0:
return number
else:
return None
print(find_even([1, 3, 5])) # None
print(find_even([1, 4, 5])) # 4
在搜索场景中,也可以用 any()、all() 或 next() 表达意图;不要为了使用 else 而让简单逻辑变得难读。
迭代字典、集合和文件
settings = {"theme": "light", "page_size": 20}
for key in settings:
print(key)
for value in settings.values():
print(value)
集合没有稳定的索引顺序,不应依赖其遍历顺序来产生业务结果。需要稳定顺序时,可以先排序:
for value in sorted({3, 1, 2}):
print(value) # 1、2、3
文件对象可以逐行迭代,适合处理大文件:
with open("events.log", encoding="utf-8") as file:
for line in file:
if "ERROR" in line:
print(line.rstrip())
文件会按需读取,不会一次把全部内容加载到内存。with 代码块结束后文件会自动关闭。
迭代时不要随意修改容器
遍历列表时删除元素,可能导致元素被跳过:
numbers = [1, 2, 3, 4, 5]
for number in numbers:
if number % 2 == 0:
numbers.remove(number)
print(numbers) # 结果不符合“删除所有偶数”的直觉
更清晰的做法是创建新列表,或者遍历副本:
numbers = [1, 2, 3, 4, 5]
numbers = [number for number in numbers if number % 2 != 0]
print(numbers) # [1, 3, 5]
遍历字典时改变字典大小通常会抛出 RuntimeError。可以先复制键或项目:
scores = {"Ada": 95, "Lin": 88}
for name in list(scores):
if scores[name] < 90:
del scores[name]
惰性迭代与一次性消费
许多函数返回迭代器,例如 map()、filter()、zip() 和 enumerate():
numbers = map(int, ["1", "2", "3"])
print(list(numbers)) # [1, 2, 3]
print(list(numbers)) # [],已经消费完
这种惰性行为可以减少中间列表和内存占用,但也意味着结果不能随意重复遍历。需要多次访问时,应显式保存为列表或重新创建迭代器:
numbers = list(map(int, ["1", "2", "3"]))
何时使用哪种写法
- 只需逐项执行副作用(打印、写文件):使用
for; - 需要转换或筛选并得到新列表:考虑列表生成式;
- 数据量很大或来源是流:使用生成器或其他惰性迭代器;
- 只需要判断是否存在或全部满足:优先考虑
any()、all(); - 需要索引或并行数据:使用
enumerate()、zip(),不要手写下标循环。
常见错误
把可迭代对象当成迭代器
列表支持 for,但不支持直接 next(numbers)。先调用 iter(numbers) 才能取得迭代器:
numbers = [1, 2, 3]
# next(numbers) # TypeError
print(next(iter(numbers))) # 1
忘记迭代器会被消耗
zip()、map() 和生成器通常只能从头到尾消费一次。需要重复使用时,保存结果或重新创建来源。
用 range(len(values)) 代替直接遍历
只有在确实需要索引来修改或访问邻近元素时才使用下标。其他情况下,直接遍历元素更短,也不容易发生越界。
练习
- 使用
enumerate()给列表['学习', '练习', '复习']编号,编号从1开始。 - 使用
zip()把姓名和成绩组成字典,并处理两个列表长度不一致的情况。 - 编写循环,在一组数字中查找第一个能被
7整除的数;找不到时输出提示。 - 解释为什么在遍历字典时直接新增键会出错,并写出安全的修改方式。
- 分别用
for循环和列表生成式筛选出range(20)中的 3 的倍数。
小结
- 迭代是按顺序逐项处理数据,
for是最常用的表达方式。 for通过iter()和next()取值,遇到StopIteration时结束。- 列表、字符串、字典、集合、
range和文件对象等都可以迭代。 enumerate()取得索引,zip()按位置组合多个来源;二者都返回惰性迭代器。break、continue和for ... else可以控制循环流程。- 不要在迭代中随意改变容器结构;注意迭代器只能消费一次。