我最早意识到迭代器这东西必须自己动手写,不是在学校学语法的时候,而是在一个深夜排查线上问题的现场。当时我们的服务要批量处理一份十几GB的日志文件,第一版代码特别简单,直接 open(file).readlines() 然后循环处理。结果程序启动没几分钟,内存直接飙到几个G,紧接着整个容器被 OOM Killer 干掉了。后来把读取方式改成逐行迭代,内存占用瞬间降到几十MB,同一个任务从"跑不起来"变成了"轻松跑完"。
那次之后我明白了一个道理:迭代器不是一个语法糖,而是一套关于"按需生产数据"的设计思想。自定义迭代器看起来只是实现 __iter__ 和 __next__ 两个方法,但真正用好的时候,它能让你的代码在内存、性能、可读性上同时受益。这篇文章我想从设计者的视角,把自定义迭代器这件事从头到尾拆开讲清楚,包含协议原理、实战案例、踩坑记录和性能分析,希望能帮你少走一些弯路。
如果你正准备处理大文件、构造数据管道、给自定义容器加遍历能力,或者只是想把 for 循环背后的机制彻底搞明白,这篇文章都适合你。我会用Python为主、JavaScript为辅做对比讲解,因为这两种语言里迭代器都是高频核心机制,而且设计思路高度相似。
1. 一个让内存直接爆掉的需求:为什么非得自己写迭代器
先说个真实场景。我接手过一个数据同步模块,需求是从外部接口拉取用户行为数据,逐条清洗后写入数仓。接口的输出是一个很大的JSON数组,正常情况一次返回几万条记录。第一版代码长这样:
python复制import requests
def fetch_all_users():
response = requests.get("https://api.example.com/user-events", timeout=30)
data = response.json()
return data["items"]
然后主函数里这样做:
python复制def sync():
items = fetch_all_users()
for item in items:
clean_and_write(item)
表面看没问题,接口返回一个数组,循环遍历,一条条处理。但实际上有两个隐患。
隐患一:response.json() 会把整个响应体一次性加载到内存。如果一个接口返回50万条记录、每条2KB,一共就是1GB。再加上解析后Python对象的开销,内存占用会放大到好几倍。服务器只有4G内存的话,直接就是OOM的风险。
隐患二:通过 requests.get 拿到的响应已经全部加载到内存了,哪怕你改成流式读取 stream=True,如果代码里还写着 json(),一样是全部加载。
这个问题的本质是:数据量一大,"先全部拿到再处理"的思路就走不通了。你要的其实是一个一个地拿数据、处理、丢掉,再拿下一个——这个"一次拿一个"的能力,正是迭代器提供的。
如果用自定义迭代器来改造,思路完全不一样。你可以让迭代器内部维护一个游标,每次 next() 只获取一条数据,无论底层数据有多大,内存里始终只有"当前这一条"。
code复制底层数据(几GB文件 / 几万条接口记录)
|
| 迭代器内部按需取数据
v
内存里只有一条
|
v
处理完就丢弃
用代码表示的话,大概是这个骨架:
python复制class UserEventIterator:
def __init__(self, api_client):
self.api_client = api_client
self.cursor = None
self.current_batch = []
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.current_batch):
self.current_batch = self.api_client.fetch_next_page(cursor=self.cursor)
self.index = 0
self.cursor = self.current_batch.get("next_cursor")
if not self.current_batch.get("items"):
raise StopIteration
item = self.current_batch["items"][self.index]
self.index += 1
return item
每次 for 循环向它要一个元素时,它只从当前批次里取一条;批次用完了,再去接口拉下一页。这样一来,无论总数据量是1万条还是1亿条,内存占用是固定的。这就是自定义迭代器最朴素也最重要的价值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迭代器协议拆解:Python和JavaScript的底层约定
在动手写自定义迭代器之前,得先把协议本身弄清楚。很多人写过迭代器,但说不清协议为什么这么设计。这里我分别讲一下Python和JavaScript两边的约定,然后解释背后的通用逻辑。
2.1 Python的迭代器协议:iter 和 next
Python的迭代器协议由两个方法组成:
__iter__(self):返回迭代器对象自身,或者返回一个新的可迭代对象。for循环一开始会调用这个方法,拿到的返回值就是要用的迭代器。__next__(self):每次循环取一个元素时调用。返回当前元素;如果没元素了,抛出StopIteration异常。
注意一个关键点:for 循环不检查返回值是否为 None,它只依赖异常来结束循环。也就是说,如果没有元素了却还在 __next__ 里返回了一个"空值",循环会永远跑下去。这是个容易踩的坑。
一个最简自定义迭代器长这样:
python复制class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
使用的时候:
python复制for num in CountDown(3):
print(num)
# 输出:
# 3
# 2
# 1
为什么 __iter__ 要返回 self?因为 __iter__ 的用途是"给出一个迭代器",而如果这个对象本身就已经是迭代器(有 __next__),那直接返回自己就行。这样同一个对象既能被 for 循环消费,也能传给 iter() 函数。不过要注意,一个对象也可以只实现 __iter__ 而不实现 __next__,这叫做"可迭代对象",而 __iter__ 每次都返回一个全新的迭代器。比如一个列表就是这样,每次 for 都能从头开始遍历。
2.2 JavaScript的迭代器协议:Symbol.iterator 和 next
JavaScript的协议思路和Python完全对得上,只是名字不同。
javascript复制const countDown = {
current: 3,
[Symbol.iterator]() {
return this;
},
next() {
if (this.current <= 0) {
return { done: true, value: undefined };
}
return { done: false, value: this.current-- };
}
};
for (const num of countDown) {
console.log(num);
}
// 输出:3, 2, 1
区别在于,JavaScript的 next() 不通过异常标记结束,而是返回 { done: boolean, value: any },用 done: true 表示没有更多元素。Python里收到一个没有元素信号的方式是异常,收到异常的成本在CPython里相对较高,所以性能敏感时通常用 default 参数或者先判断长度。JavaScrip
