你有没有想过for循环到底是怎么把数据一个个取出来的我当年刚开始学 Python 的时候写for i in some_list那叫一个顺手。直到有一天隔壁组的同事问我那如果 some_list 不是列表换成生成器呢 我愣住了。后来翻开源码、看官方文档又自己手写了个迭代器才真正明白——原来for循环根本不是循环而是一场迭代器和可迭代对象之间的默契配合。这篇文章我就用最容易理解的方式把Python迭代器Iterator背后那套机制掰开揉碎讲清楚。从for循环的执行流程、__iter__和__next__的协议细节到怎么手写迭代器、怎么用生成器偷懒再到大家踩过最多的坑包括我自己的黑历史全都会覆盖到。无论你是刚学 Python 的新手还是写过一阵子项目的老手这篇文章都能让你在循环这件事上少走弯路。1. 先搞清楚一件事for 循环到底在循环什么1.1 一个让我惊讶的事实for 循环不是循环很多人跟我以前一样把for理解成 C 语言里的循环觉得它就是在反复执行一段代码、用一个变量去取某个位置上的数据。但在 Python 里这个理解是错的。for的本质是遍历iterate——它不是在循环而是在消费一个迭代器直到对方告诉你没货了。我给你看一个最简单的证据。在 Python 里你可以写出这样的代码for i in 5: print(i)结果抛出的异常是TypeError: int object is not iterable。注意它说的是 notiterable不是 notloopable。因为在 Python 世界里能被for遍历的前提是对象实现了迭代协议。这个协议就是这整篇文章的核心。1.2 手动模拟一次 for 循环的执行过程为了验证我上面的说法我当初写了一个不带for关键字的假 for 循环效果一摸一样def my_for(iterable, func): # 第一步拿到一个迭代器 iterator iter(iterable) # 第二步循环调用 next一次拿一个 while True: try: value next(iterator) except StopIteration: # 迭代器告诉我们没货了结束 break # 第三步把取到的值交给用户函数处理 func(value) my_for([1, 2, 3], lambda x: print(x))这段代码完美复刻了for的内部行为一共就三个动作iter(iterable)—— 从可迭代对象身上拿到一个迭代器。next(iterator)—— 每轮从迭代器里取出一个元素。StopIteration异常 —— 当取不到元素时迭代器抛出这个异常for循环捕获后就乖乖退出。你平时写一万次for循环Python 源码里干的事跟我上面这段几乎一模一样。看懂了这段你就算彻底入门了。1.3 为什么 Python 要设计成这样你可能会问干嘛不直接用i 0; i len(list); i 1这种下标访问Python 用这个迭代器协议到底图什么我个人的理解是三个字统一性。列表有下标可以用索引那字典呢集合呢文件对象呢要是每个类型都设计一套自己的遍历方式那for循环就得为每种类型写不同分支了。迭代器协议把这一切统一成一个接口——只要你有__iter__和__next__我for就能无脑取。这个设计和 Java 的Iterator接口、C 里的迭代器模式其实是同一个思路只是 Python 用协议 异常的方式把它藏得更深用起来更省心。2. 可迭代对象和迭代器傻傻分不清的两个核心概念2.1 一张图讲清两者的关系这是初学者最容易搞混的地方我把道理讲透。可迭代对象Iterable是可以被遍历的对象迭代器Iterator是负责遍历的对象。for拿到的是可迭代对象但真正一个一个取数据的是迭代器。用生活类比可迭代对象是一本菜单迭代器是上菜的服务员。你可以一直盯着菜单看反复拿到新的迭代器但不是菜单本人在给你上菜服务员才是那个负责一道菜一道菜端到你面前的人。服务员的记性迭代器状态在一次上菜过程中是会变的菜上完了服务员这一轮的工作就结束了。2.2 怎么检查一个对象是哪种角色Python 给出了两个标准方法isinstance(obj, Iterable)和isinstance(obj, Iterator)。但需要注意类名容易误导人。from collections.abc import Iterable, Iterator lst [1, 2, 3] print(isinstance(lst, Iterable)) # True —— 列表是可迭代的 print(isinstance(lst, Iterator)) # False —— 但列表本身不是迭代器 it iter(lst) print(isinstance(it, Iterable)) # True —— 迭代器一定是可迭代的 print(isinstance(it, Iterator)) # True —— 但它还是迭代器这引出一个关键结论迭代器一定是可迭代对象但可迭代对象不一定是迭代器。列表、字典、集合、字符串都是可迭代对象但它们都没有__next__方法所以不是迭代器。真正的迭代器是通过调用iter()从这些对象身上提取出来的。2.3 为什么列表能反复 for迭代器只能走一遍很多人的困惑在于为什么for列表可以循环一百遍都没事for一个生成器第二遍就空了 秘密就在iter()这个方法上。列表的__iter__每次被调用时会返回一个全新的迭代器记录从第 0 个位置开始的遍历状态。所以你每次都从头开始自然怎么遍历都不嫌腻。而迭代器本身的__iter__方法返回的是它自己——这就意味着迭代器一旦被创建它内部就带有一个当前位置的指针每next一次指针就往前走一步绝不回头。走完了指针到末尾这个迭代器就彻底没用了。比如这个经典场景几乎每个 Python 程序员都踩过numbers [1, 2, 3, 4, 5] it iter(numbers) print(next(it)) # 1 print(next(it)) # 2 # 此时列表本身还是完整的可以继续从头遍历 for n in numbers: print(n) # 1 2 3 4 5 # 但迭代器 it 已经走到了一半继续 next 就是 3、4、5 print(list(it)) # [3, 4, 5]理解了这一点很多诡异 bug 就已经能躲掉一半了。3. 手写一个迭代器从零实现完整协议3.1 最小流程把iter和next成套实现如果你要自己定义一个迭代器协议要求你必须实现两个方法缺一不可__iter__(self)返回迭代器自身。虽然听起来像废话但它是协议的一部分保证迭代器本身可以被for。__next__(self)返回下一个元素。如果没元素了就抛StopIteration。我写项目时最常用的是倒计时器代码短但五脏俱全class CountDown: def __init__(self, start): self.current start def __iter__(self): # for 循环碰到这个对象时会调用 iter() return self def __next__(self): # 倒计时值为 0 之前每次返回值都减 1 if self.current 0: raise StopIteration # 没货了就抛异常让 for 停下 self.current - 1 return self.current 1 for n in CountDown(5): print(n) # 输出 5 4 3 2 1这里我特意讲一下异常的设计哲学。Python 是EAFPEasier to Ask for Forgiveness than Permission先做再请求原谅风格它不流行用布尔值返回 标志位检查来告诉外面我到底还有没有下一个而是直接用异常来做终止信号。这样有个好处调用方写起来非常干净一个for加一个try不用每个next都手动检查条件。3.2 进阶实操写一个无限序列的斐波那契迭代器工作中经常会遇到不知道到底要算到第几个才算完的场景。无限序列用迭代器表达特别优雅因为计算是惰性的你不next它就不算。class Fibonacci: def __init__(self, limitNone): self.limit limit self.a, self.b 0, 1 self.count 0 def __iter__(self): return self def __next__(self): if self.limit is not None and self.count self.limit: raise StopIteration self.a, self.b self.b, self.a self.b self.count 1 return self.a fib Fibonacci(10) for num in fib: print(num) # 1 1 2 3 5 8 13 21 34 55说句踩过坑之后的总结迭代器内部的状态最好集中维护在少数几个字段里。像这里的a、b、count一旦你开始给迭代器加各种分支状态判断代码很快就会乱成一团。如果真觉得维护状态很繁琐那说明你需要用下一节的生成器了。3.3 实际应用用迭代器读大文件内存不再爆炸我有个实际项目是在处理几 GB 的日志文件如果用readlines()把整个文件读取到内存机器直接卡死。而文件对象本身就是迭代器我们可以写一个通用的分块读取迭代器完全按需加载class ReadChunks: def __init__(self, file_path, chunk_size1024): self.file open(file_path, r, encodingutf-8) self.chunk_size chunk_size def __iter__(self): return self def __next__(self): chunk self.file.read(self.chunk_size) if not chunk: # 没内容了就是文件读完了 self.file.close() raise StopIteration return chunk for chunk in ReadChunks(huge.log, 4096): process(chunk) # 每次只处理 4KB 数据注意很多文档里推荐用with open()管理文件生命周期那在正式代码里确实更标准。我上面写的是为了让__next__逻辑紧凑真用的时候你可以在StopIteration分支里加一个显式close()兜底或者用生成器写法让with和作用域更清晰。4. 生成器省下 80% 写迭代器的力气4.1 yield 的本质函数被暂停了而不是被中断如果说迭代器协议是手动挡那生成器就是自动挡。一个函数只要里面出现了yield关键字它就不是普通函数了而是一个生成器函数。你调用它不会立刻执行函数体而是返回一个生成器对象——这个对象其实就是一个隐式的迭代器。我用一个例子感受一下暂停和恢复def count_down(start): print(开始倒计时) while start 0: yield start start - 1 print(倒计时结束) gen count_down(3) # 没有任何打印发生 print(next(gen)) # 打印开始倒计时然后输出 3 print(next(gen)) # 输出 2 print(next(gen)) # 输出 1 print(next(gen)) # 打印倒计时结束然后抛 StopIteration注意print(next(gen))的三次调用第一次进入函数时代码从第一行执行到第一个yield然后整个函数被冻结包括局部变量值、执行位置全都保留。下一次调用next(gen)函数从上次yield的地方继续而不是从头开始。这就是我说的暂停恢复。4.2 斐波那契生成器代码少一半可读性强一倍前面那个斐波那契迭代器要写六七行换成生成器就四行def fibonacci(limitNone): a, b 0, 1 count 0 while limit is None or count limit: yield a a, b b, a b count 1每次yield a暂停的时候Python 会把整个调用栈连同a、b、count的状态全部保存下来等下一次调用再恢复。这个机制的底层是栈帧冻结不用管太细记住生成器就是带记忆的函数就够了。4.3 生成器表达式列表推导式的一个节省版你肯定写过[x * x for x in range(10)]但你想没想过如果你只打算遍历一遍完全没必要把 10 个平方值都存在一个列表里换成生成器表达式用圆括号就行squares (x * x for x in range(10)) # 生成器表达式 print(squares) # generator object ... for s in squares: print(s)列表推导式会一次性算出所有元素放进内存生成器表达式则是一个一个算。数据量小看不出差别数据量大的时候比如range(10_000_000)你用列表推导式可能直接内存爆掉用生成器表达式内存占用几乎为 0。所以我的习惯是只要我不需要通过下标随机访问数据一律用生成器表达式。4.4 send往生成器里反向塞东西生成器除了可以往外yield值还能接收外面传进来的值这个方法是send(value)。虽然日常用得少但理解它你会对生成器 可暂停可恢复的程序有更透彻的认识。def axis(): x 0 while True: received yield x x received if received is not None else x 1 a axis() print(next(a)) # 0yield 把 x 传出来 print(a.send(100)) # 100外部把值传回去x 被设为 100 print(next(a)) # 101这个能力是协程的雏形很多异步框架的底层就是靠这种双向通信实现的。不过对于普通业务开发来说理解到生成器能保存状态这一步已经够了。5. 实战演练for 循环背后机制的完整复刻5.1 我如何用 while 逼真模拟一个 for 循环纸上得来终觉浅我建议你也真的把for手写一遍。这里我做一个更完整的版本支持break和continue的信号返回def simulate_for(iterable, body): it iter(iterable) while True: try: item next(it) except StopIteration: break signal body(item) if signal break: break elif signal continue: continue # 使用示例打印 1 到 10但遇到 5 就提前停 def print_item(x): print(x) if x 5: return break simulate_for(range(1, 11), print_item)虽然官方for是语言层面的语法不是真的靠信号返回实现但你会发现它和真实for的执行路径几乎一致初始化迭代器 → 捕获异常 → 执行循环体 → 判断是否继续 → 继续next。理解这个流程碰到各种为什么这里多跑了一次为什么那里少跑了一次的怪问题时你就能自己推断了。5.2 提前退出和无限迭代最容易被坑的地方现实中很多循环数据集是无穷无尽的。比如流式消息、网络包、传感器读数。你没法预知总量只能一个个往下取。此时迭代器的惰性特性就派上大用场了配合itertools标准库能玩出花来。import itertools # 无限计数器 for i in itertools.count(10, 2): # 10, 12, 14, ... if i 20: break # 无限重复某个序列 for item in itertools.cycle([A, B, C]): # 永不停止必须用 break break # 取前 N 个有限值 for item in itertools.islice(itertools.count(0), 5): print(item) # 0 1 2 3 4使用itertools写无限数据流是我特别喜欢的用法它让我们把边界控制完全交给调用方去break而生产方根本不用关心到底有多少数据。这在事件处理、轮询系统的代码里特别干净。5.3 标准库里到处都是迭代器的身影写代码久了你会发现Python 标准库中凡是跟遍历一条条数据有关的函数几乎都是基于迭代器协议的。enumerate返回的是一个枚举对象zip合并多个序列返回的也是一个可迭代的 zip 对象range在 Python 3 里并不生成列表它本身就是一个惰性序列。文件对象open()返回的东西直接就能for line in f底层就是每次next(f)读取一行。with open(data.txt) as f: for line in f: # 这行没有 readlines没有列表内存友好 print(line.strip())官方文档也写着fileobjects are their own iterators——文件对象本身就是迭代器。你越深入越会发现整个 Python 数据处理的血液里流的就是迭代器。6. 常见问题与排查技巧实录6.1 明明打印有值sum / list / max 却返回空这个坑 99% 的新手都会踩而且是排查半天发现是迭代器被消费完了的典型。看这段data (x * 2 for x in range(5)) print(sum(data)) # 20第一次迭代正常 print(sum(data)) # 0迭代器已经空了为什么第二次是 0因为sum内部本质上是在做for循环它会从头到尾next一遍把整个生成器消费完。第一遍已经到底了第二遍自然什么都取不到。迭代器不可复用是它的根本属性。解决办法也很直白如果你真需要重复遍历就把数据转成列表缓存起来或者每次从头重新生成一个新迭代器。6.2 为什么 next() 一直疯狂报 StopIteration如果你在循环里手动调next()且没有处理异常程序就会直接崩溃it iter([1]) print(next(it)) # 1 print(next(it)) # 抛出 StopIteration程序就挂了这是正常的StopIteration就是协议的一部分for循环会捕获它但你的裸调用不会。想避免崩溃要么用default_value next(it, fallback)给个默认值要么主动用try/except包起来。在写外部要捕捉的迭代器时我习惯用next(it, None)做边界处理但要注意如果你的数据里本身存在合法的None这就变得危险了建议换成哨兵值或异常。6.3 Python 3 的 range 到底是什么很多人从 Python 2 转过来后一直担心range会生成巨大列表。其实 Python 3 的range返回的是一个range对象它是惰性计算的、支持长度和索引的序列类型不是迭代器也不是一次性生成所有元素的列表。不信你可以看range(10**10)定义这一行几乎不占内存。判断它是不是迭代器iter(range(10))会返回一个range_iterator这说明range本身是可迭代对象而不是迭代器。所以你可以反复for i in range(10)一万遍完全没问题范围对象不会因为被遍历了一次就枯竭。6.4 自定义对象放进 for 循环报 TypeError 的排查套路当你的自定义类for item in my_obj报错TypeError: MyClass object is not iterable时按照三个步骤排查确认类里有没有定义__iter__方法。如果没有for根本拿不到迭代器。如果定义了__iter__确认它return的是一个迭代器而不是写在return self却又没有__next__的情况。如果__iter__返回的是其它对象确认那个对象正确实现了__next__并且在耗尽时抛StopIteration。这三条是我在代码 review 中被问得最多的问题。7. 性能、进阶与我的体会7.1 迭代器是否比列表更快内存和速度要分开看很多人想当然认为迭代器快这个说法不够准确。迭代器最大的优势是内存省、响应快——由于它按需计算你永远不需要一次性把全量数据搬进内存。但在单次取值的 CPU 开销上迭代器通常不会比列表访问慢多少甚至在一些场景下略慢因为它多了next()函数调用和异常处理的开销。我处理大规模数据分析时的策略是如果数据量超过内存的 1/10优先用迭代器或生成器如果只是几十万的小数据直接用列表代码更简单。不要为了显得高级而盲目把所有列表推导式改成生成器表达式性能收益小还容易给自己后续调试带来麻烦。另一个值得注意的细节是iter()的拆包引出的惰性特性a, b, *rest iterator # 可以和解包一起用解包本质上也是在消费迭代器而惰性让你可以优雅地把前几个重要、后面的兜底这样的业务逻辑直接表达出来。7.2 迭代器在框架和库中无处不在你会发现很多框架的高级功能都离不开迭代器。Django 的 QuerySet 本身是可迭代的所以你可以直接for post in posts它会在底层按需从数据库加载记录。pandas 的read_csv提供了chunksize参数返回的 TextFileReader 是一个迭代器让你一块一块地读取超大 CSV 而不用装进内存。Spark 的 RDD 操作更是典型的懒执行迭代器思想——整个计算过程都是惰性的你行动操作的时候才真的开始计算。理解了 Python 的迭代器你理解这些框架会容易得多。7.3 我踩过的一个真实大坑去年有个数据清洗项目我需要多次过滤同一份日志数据。刚开始图省事我把日志读取封装成一个生成器函数read_logs()然后直接filtered_1 [x for x in read_logs() if ...]、filtered_2 [x for x in read_logs() if ...]。第一遍没问题但我为了省事把logs read_logs()存下来复用结果第二遍遍历的时候因为生成器已经耗尽所有结果为空。当时查了整整半小时最后发现是这个一次性的坑。从那以后我给自己定了个规矩凡是要多轮遍历的数据第一步先转成 list 或 tuple 快照。宁可多吃内存也不在逻辑上埋地雷。7.4 最后分享一个小技巧如果你正在写一个需要逐个处理海量数据且处理逻辑比较复杂的程序别急着定义__next__类先试着用生成器函数搭配yield from拆分任务。yield from允许你在一个生成器里委托给另一个生成器或迭代器代码结构可以相当清晰def lines_from_files(file_paths): for path in file_paths: with open(path, r, encodingutf-8) as f: yield from f这样一个生成器就能串联多个文件逐行输出内存消耗始终恒定。你在处理日志合并、数据流汇总等场景时会发现这个模式极其顺手。我对迭代器的整体体会是它不只是语法糖而是 Python 数据抽象的灵魂之一。一旦你从循环变量的思维切换到迭代器消费的思维写出来的代码在内存占用、代码复用和抽象层次上都会有质的提升。希望这篇拆解能帮你在for循环这条路上看到一个更大的世界。