AI 技术博客
返回首页
Python 基础 9 分钟阅读

迭代协议:iter() 与 next() 与 for 的实现原理

Python迭代器生成器

这是第十一章的第 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__ 的迭代器。

  1. 写一段代码,验证 range(5) 是可迭代对象但不是迭代器,并用 iter() 得到迭代器后再逐次 next() 直到耗尽。
  2. 下面这段代码会打印什么?先猜测,再运行验证。
it = iter([10, 20])
print(next(it))
print(next(it, 'done'))
print(next(it, 'done'))
  1. 思考题:Python 的 for 循环在 CPython 内部对 StopIteration 做了特殊优化(不再走完整的异常处理路径)。这意味着什么?它对手写 while True: try: next(it) except StopIteration: break 有什么影响?