这是第十一章的第 1 篇。第三章的 for 循环你大概写过很多遍——for x in lst: ...——但有没有想过,这个语法糖背后到底发生了什么?Python 的 for 不是”按索引遍历”,而是走一套完整的迭代协议。理解了协议,你就能写出任意类型的可迭代对象,也才能真正读懂后续生成器、itertools、协程这些高级特性。
本篇回答三个问题:什么是可迭代对象、什么是迭代器、for 循环的机械展开长什么样。读完本篇,你会明白为什么列表能反复遍历而迭代器只能走一次,以及 iter() 和 next() 这对组合是 Python 所有循环机制的地基。
可迭代对象 vs 迭代器 vs 序列
这三个术语在 Python 里经常被混用,但它们有明确的层级关系:
- 可迭代对象(Iterable):实现了
__iter__方法(返回一个迭代器)或__getitem__方法的对象。列表、字符串、字典、元组、range 都是可迭代对象。 - 迭代器(Iterator):实现了
__iter__和__next__方法的对象。__iter__返回自身,__next__返回下一个值,没有值时抛StopIteration。迭代器是一种状态机,它记住了”我走到了哪一步”。 - 序列(Sequence):同时满足可迭代、有整数索引、有
__len__的对象。列表、字符串、元组、range 都是序列。
一句话区分:可迭代对象是”可以被迭代的”,迭代器是”正在进行的迭代”,序列是”可以按位置访问的可迭代对象”。一个对象可以同时是三者,也可以只属于其中一两个。
迭代协议:iter() 与 next()
Python 的迭代协议由两个内置函数驱动:iter(obj) 和 next(it)。
调用 iter(obj) 时,Python 会检查 obj 是否有 __iter__ 方法:
- 如果有,返回一个迭代器;
- 如果没有但有
__getitem__,Python 会隐式创建一个按索引 0,1,2… 访问的迭代器(这是旧式协议)。
拿到迭代器后,反复调用 next(it):
- 返回当前值并把迭代器推进到下一步;
- 到达末尾时抛
StopIteration异常。
这就是 for 循环的完整底层逻辑:
for x in iterable:
# 循环体
等价于:
iterator = iter(iterable)
while True:
try:
x = next(iterator)
# 循环体
except StopIteration:
break
注意三件事:其一,iter(iterable) 每次调用都会产生一个新的迭代器对象(对于列表这样的可迭代对象而言);其二,next(iterator) 在没有参数时抛 StopIteration,但可以传默认值 next(it, default),此时到达末尾不会报错而是返回默认值;其三,StopIteration 是一个异常,但在 Python 3.7+ 中,CPython 对 for 循环做了内部优化,直接把 StopIteration 转为循环终止,不再经过正常的异常处理路径。
先看一段最朴素的 for 循环,然后用显式 iter + next 手动展开,观察两者是否等价:
nums = [10, 20, 30]
# 方式一:for 循环
print("for 循环:")
for n in nums:
print(n, end=" ")
print()
# 方式二:手动展开
print("手动展开:")
it = iter(nums)
while True:
try:
n = next(it)
print(n, end=" ")
except StopIteration:
break
print()
for 循环:
10 20 30
手动展开:
10 20 30
两者输出完全一致。这说明 for 循环只不过是一段更简洁的 iter + next + try/except StopIteration 的组合。
再验证一个关键性质:列表是可迭代对象但不是迭代器。调用 iter() 会返回一个新的迭代器对象,两次调用返回的对象 id 不同:
nums = [1, 2, 3]
it1 = iter(nums)
it2 = iter(nums)
print(id(it1), id(it2)) # 两个不同的 id
print(it1 is it2) # False
print(next(it1)) # 1
print(next(it2)) # 1(新迭代器从头开始)
140234864682496 140234864682432
False
1
1
现在看一个反例:迭代器本身调用 iter() 返回自身(因为迭代器的 __iter__ 就是 return self):
it = iter(nums)
print(it is iter(it)) # True:迭代器就是它自己的迭代器
True
这就解释了为什么迭代器只能使用一次——因为调用 next() 会永久推进内部状态,没有重置机制。而列表每次 iter() 都给你一个新的迭代器,所以可以反复遍历。
next() 带默认参数的用法在很多真实场景里很有用,比如读取文件最后一行:
lines = iter(["a", "b", "c"])
print(next(lines, "EMPTY")) # a:正常取出
print(next(lines, "EMPTY")) # b
print(next(lines, "EMPTY")) # c
print(next(lines, "EMPTY")) # EMPTY:已耗尽,返回默认值
a
b
c
EMPTY
易错点 1:误以为列表是迭代器。 列表能放进 for 循环,但不是迭代器——它没有 __next__ 方法。下面两段代码结果一样,但机制完全不同:
nums = [1, 2, 3]
print(hasattr(nums, '__next__')) # False:列表不是迭代器
print(hasattr(iter(nums), '__next__')) # True:迭代器才有 __next__
False
True
如果你看到某个代码写的是 nums.__next__(),它必然报错 —— AttributeError:list 对象没有 next 方法。正确做法是先 iter(nums) 拿到迭代器再 next()。
易错点 2:以为迭代器可以重置。 这是最经典的坑:
it = iter([1, 2, 3])
print(list(it)) # [1, 2, 3]
print(list(it)) # []:迭代器已经耗尽!
[1, 2, 3]
[]
迭代器是一次性的,它不像列表可以反复遍历。如果你需要多次遍历同一数据,应该保存原始的可迭代对象,每次都调用 iter() 创建新迭代器,或者用 itertools.tee() 克隆迭代器(第 5 篇会展开)。
本篇建立了 Python 迭代协议的基础认知:可迭代对象实现 __iter__,迭代器实现 __iter__ 和 __next__,for 循环本质上就是 iter() + next() + 捕获 StopIteration 的 while 循环。列表是可迭代对象但不是迭代器,每次 iter() 产生新迭代器;迭代器是一次性的,不能重置。掌握了这套机制,后续生成器、itertools 乃至协程的讲解都会顺理成章——它们都是在这套协议之上叠加的能力。下一篇进入生成器:如何用 yield 写出一个无需手动实现 __iter__/__next__ 的迭代器。
- 写一段代码,验证
range(5)是可迭代对象但不是迭代器,并用iter()得到迭代器后再逐次next()直到耗尽。 - 下面这段代码会打印什么?先猜测,再运行验证。
it = iter([10, 20])
print(next(it))
print(next(it, 'done'))
print(next(it, 'done'))
- 思考题:Python 的
for循环在 CPython 内部对StopIteration做了特殊优化(不再走完整的异常处理路径)。这意味着什么?它对手写while True: try: next(it) except StopIteration: break有什么影响?