Python 的 for 到底怎么跑起来的?拆开迭代协议:GET_ITER 之后 FOR_ITER 在循环什么

写 Python 的人天天用 for,但多数人说不清 for x in xs 在字节码层面到底发生了什么。for 能遍历 list、str、dict、文件、生成器------这些类型八竿子打不着,凭什么 for 一视同仁?答案不在这些类型里,在它们共同遵守的那份迭代协议里。本文用 CPython 3.13.14 实机数据,把这份协议拆开。

一、先看字节码:for 循环其实只有三条指令

把一个最简单的 for 编译出来,结构极其简单:

vbnet 复制代码
RESUME           0
LOAD_FAST        0 (xs)
GET_ITER                 ← 关键第一步
L1:  FOR_ITER     3 (to L2)     ← 循环体每次跳回这里
     STORE_FAST   1 (x)
     JUMP_BACKWARD 5 (to L1)
L2:  END_FOR
     POP_TOP
     RETURN_CONST 0 (None)

三个关键指令说明了一切:GET_ITER 拿迭代器、FOR_ITER 取下一个、END_FOR 善后 。for 自己什么都不遍历,它只是把活儿全外包给了一个叫「迭代器」的东西。

二、迭代协议:两个方法的约定

for 对对象的要求只有两条,写在数据模型里:

  • __iter__():返回一个迭代器
  • __next__():返回下一项,没有就抛 StopIteration

实测一个手写协议的类:

ruby 复制代码
class NewProto:
    def __init__(self, n): self.n = n; self.i = 0
    def __iter__(self): return self
    def __next__(self):
        if self.i >= self.n: raise StopIteration
        v = self.i; self.i += 1; return v

for x in NewProto(3) 得到 [0, 1, 2],耗尽后再 next() 抛 StopIteration。for 循环的退出条件不是「数到 3」,而是 __next__ 抛出 StopIteration------for 循环甚至不知道自己在遍历几个元素。

三、老协议还活着:只有 __getitem__ 也能迭代

不写 __iter__ 行不行?也行。这是最反直觉的事实:

ruby 复制代码
class OldProto:
    def __getitem__(self, i):
        if i >= len(self.data): raise IndexError
        return self.data[i]

for x in OldProto(3) 照样输出 [0, 1, 2]。iter() 会把它包成一个 sequence-iterator,用下标 0、1、2...... 去试,直到 IndexError。这是 Python 2 时代的老协议,为了兼容保留至今------你的类只要支持下标,for 就能遍历它。

四、iter() 的三副面孔

iter() 根据参数个数切换行为,这点文档里常被忽略:

调用形式 行为 实测
iter(可迭代对象) 返回其 __iter__() 的结果 iter(l) 每次新建,两个对象 is not
iter(可调用对象, 哨兵值) 反复调用,直到返回值等于哨兵 iter(reader, b'__END__') 分块读
iter(迭代器) 返回自身 iter(enumerate('ab')) is 那个 enumerate → True

双参形式是 C 里 while (c = read()) != STOP 的 Python 化,读文件流、读 socket 的老代码里到处都是。

五、可迭代 vs 迭代器:一字之差,两种命运

这是面试重灾区,实测区分:

  • 可迭代对象 (list、str、dict):iter(l) 每次调用新建一个独立 迭代器,两个 is not 成立,各自推进互不干扰
  • 迭代器 (generator、enumerate、zip、文件对象):iter(it) is it 恒为 True------迭代器的 __iter__ 返回自己

推论:迭代器是一次性的 。it = iter([1,2,3]); list(it); list(it) 第二次得到 [],因为第一次已经耗尽。

六、性能:for 比 while+next 快 3.89 倍

100 万个元素实测(CPython 3.13.14):

写法 耗时 相对
for x in data 26.8 ms 1.00x
while + next() 手动拉 104.5 ms 3.89x
列表推导 [0 for x in data] 41.8 ms 1.56x

慢在哪不在 next() 函数本身,在于 Python 级 while 循环每次迭代都要重新走一遍字节码分发,而 FOR_ITER 在解释器里是内建快路径 ------单个指令完成「调用 __next__、捕获 StopIteration、跳转」三件事,没有 Python 函数调用开销。

七、异常穿过 for:迭代器记得断点

一个少有人注意的细节:

python 复制代码
it = iter([1,2,3,4])
try:
    for x in it:
        if x == 2: raise ValueError
except ValueError: pass
list(it)   # → [3, 4]

异常穿透 for 后,迭代器从断点继续,不是从头开始。 迭代器的位置状态存在迭代器自己身上,for 循环的提前退出(break、异常)都不会重置它。这就是「生成器只能遍历一次」的机制根源。

八、实战守则

  • 能 for 就别 while+next:3.89 倍的差距,写的代码还更短
  • 需要下标才用 enumerate :for i, x in enumerate(xs),别写 range(len(xs))------enumerate 本身就是迭代器(iter(e) is e),协议复用
  • 协议类型检查 :for 遇到 __iter__ 返回非迭代器的对象会抛 TypeError: iter() returned non-iterator of type 'int'------这是协议检查在保护你
  • 一次性消费 :把生成器当参数传进两个函数,第二个拿到的是空壳,要用 itertools.tee 或先物化

一句话总结:for 循环是迭代协议的发动机,__iter__ 是钥匙孔,__next__ 是曲轴,StopIteration 是熄火线。理解了这条链,文件对象、生成器、zip 的「一次性」行为全都顺理成章。

相关推荐
凯瑟琳.奥古斯特1 小时前
动态调优IoU提升OCR文本合并精度
开发语言·python·算法
蒸鱼Yuzheng2 小时前
自动化长任务治理:租约、心跳、孤儿进程与安全回收
自动化测试·python·任务调度·可靠性·分布式系统
2601_962885723 小时前
从 baostock 迁移到 AlphaFeed:接口对照与代码迁移指南
python
AI情绪识别开源3 小时前
检信ALLEMOTION 标定治理与信号质量闸门 · 老年无感认知评估分析器
python
七夜zippoe3 小时前
启元 Q1 腿部运动学逆解:Python 算站立下蹲关节角
开发语言·python·ai·机器人·腿部运动学
第404号工位3 小时前
插上充电宝真能偷光相册?我拆解了一台改装充电宝,实测“恶意充电”的真实攻击链
linux·python·架构
技术卷4 小时前
Windows 下自定义 MCP 踩坑:stdin 默认继承导致 CPython 启动阻塞
python·mcp·zcode
小oo呆4 小时前
【学习心得】Python多进程
python·操作系统
CodeGL4 小时前
9、Python基础之IO编程
开发语言·数据库·python