Python进阶系列之-迭代器与生成器:榨干内存的“懒加载”魔法
写在前面:在实际开发中,如果我们要处理一个包含1000万条数据的日志文件,或者生成一个1亿个元素的列表,你的第一反应是不是直接写个列表存起来?小心!这会直接把电脑内存撑爆,程序卡死!为了解决这个问题,Python提供了一套“懒加载”机制——迭代器与生成器。今天我们就来揭开它们神秘的面纱,看看Python是如何做到“用时才取,用完即焚”的。
文章目录
一、前置知识:什么是迭代(Iteration)?
在讲迭代器之前,我们先理清几个容易混淆的概念。
在Python中,凡是可以通过 for ... in ... 遍历的对象,统称为可迭代对象。
比如我们常用的列表、字典、集合、字符串,甚至文件对象。
💡 如何判断一个对象是否可迭代?
可以使用isinstance(obj, Iterable)来判断。
from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance(123, Iterable)) # False
但这里有个坑:可迭代对象不一定是迭代器!
- 列表、字典等:是可迭代对象,但不是迭代器。它们把所有数据都存在内存里,可以通过索引或键随机访问。
- 迭代器:不仅可迭代,而且是一种惰性计算的对象。它内部不存所有数据,只记录当前遍历的位置,只能往前走,不能后退。
二、迭代器:顺藤摸瓜的“单行道”
2.1 什么是迭代器?
迭代器是一个可以记住遍历位置的对象。它从集合的第一个元素开始访问,直到所有的元素被访问完结束。它只能往前,不能后退。
2.2 iter() 与 next()
把一个可迭代对象变成迭代器,需要用 iter() 函数;从迭代器里取下一个值,需要用 next() 函数。
# 1. 定义一个普通的列表(可迭代对象)
my_list = [10, 20, 30]
print(type(my_list)) # <class 'list'>
# 2. 将列表转换为迭代器
my_iter = iter(my_list)
print(type(my_iter)) # <class 'list_iterator'>
# 3. 使用 next() 逐个获取值
print(next(my_iter)) # 10
print(next(my_iter)) # 20
print(next(my_iter)) # 30
# 4. 取完后再取,会抛出 StopIteration 异常
# print(next(my_iter)) # 报错: StopIteration

大白话理解:列表就像是一桌满汉全席,全摆在桌子上随你吃;迭代器就像是一个自动传菜机,每次只给你端上来一盘,吃完一盘按下按钮才给你下一盘,直到后厨没菜了(抛出异常)。
2.3 for 循环的底层真相
你有没有想过,for 循环是怎么遍历各种数据结构的?其实它的底层就是调用了迭代器协议!
# 底层伪代码逻辑演示:
# for i in [1, 2, 3]:
# pass
# 等价于以下代码:
my_list = [1, 2, 3]
my_iter = iter(my_list) # 1. 创建迭代器
while True:
try:
item = next(my_iter) # 2. 循环获取下一个值
print(item)
except StopIteration: # 3. 捕获异常,结束循环
break
三、生成器:一种特殊的迭代器
我们刚才把列表转成了迭代器,但它毕竟还是把列表的数据全装进内存了,只是换了个遍历方式。如果我们想要100万个数字,总不能先创建一个100万的列表再转吧?
这时候生成器登场了!
3.1 什么是生成器?
生成器是Python中一种一边循环一边计算的机制。它本质上就是一个用户自己写的迭代器。它不需要把所有数据一次性算出来放到内存里,而是在你每次调用 next() 的时候,才算出下一个值。
3.2 创建生成器的两种方式
方式1:生成器推导式(小括号)
把列表推导式的 [] 换成 (),就得到了一个生成器。
# 列表推导式:直接生成所有数据,占用内存大
my_list = [i for i in range(1, 6)]
print(my_list) # [1, 2, 3, 4, 5]
# 生成器推导式:生成的是一个对象,几乎不占内存
my_generator = (i for i in range(1, 6))
print(my_generator) # <generator object <genexpr> at 0x...>
# 获取数据:可以用 next(),也可以用 for 循环
print(next(my_generator)) # 1
print(next(my_generator)) # 2

方式2:yield 关键字(重点!)
如果生成逻辑比较复杂,推导式写不下,就可以写一个函数,只要函数里出现了 yield 关键字,这个函数就不再是普通函数了,而是一个生成器函数。
# 需求: 获取 1 ~ 5 的生成器
def get_generator():
for i in range(1, 6):
yield i # yield会记录每次生成的数据,然后暂停并返回
# 调用函数,此时函数体内的代码并不会执行,而是返回一个生成器对象!
my_gen = get_generator()
print(next(my_gen)) # 1 (程序执行到 yield 1 暂停)
print(next(my_gen)) # 2 (从上次暂停的地方继续执行,到 yield 2 暂停)
# 剩余的可以用 for 循环遍历
for i in my_gen:
print(i) # 3, 4, 5

🚗 大白话理解
yield:
普通函数就像是一气呵成跑完的马拉松,跑到终点return交出成绩单。
带yield的生成器函数就像是走走停停的快递车。遇到yield,它就停下来把包裹(值)递给你,然后挂起休眠。等你再喊一声next(),它立刻苏醒,从上次停下的地方继续往下跑,直到下一个yield或函数结束。
四、实战演练:用生成器生成批次数据
在AI模型训练中,数据往往是几十万条。如果一次性把数据全读进内存,机器根本扛不住。标准做法是:按批次读取数据喂给模型。生成器天生就是干这个的!
需求:读取一个有5000行歌词的 jaychou_lyrics.txt 文件,要求按 8条/批次 生成数据。
import os
# 创建测试数据
test_lines = [
"我用功读书,只为有一天能飞得更高\n",
"你是我心中最美的云彩\n",
"安静的夜,我一个人在写歌\n",
"雨下整夜,我的爱溢出就像雨水\n",
"你走之后,酒暖回忆思念瘦\n",
"我用尽所有力气,只为靠近你\n",
"你的眼神,是我唯一的信仰\n",
"时间在走,我还在原地等你\n",
"爱像风,吹过就不再回头\n",
"你是我生命中最美的意外\n",
"如果爱有声音,那一定是你的心跳\n",
"我愿化作星辰,守护你的每个夜晚\n"
]
# 写入文件
with open('jaychou_lyrics.txt', 'w', encoding='utf-8') as f:
f.writelines(test_lines)
print("✅ 已成功创建测试文件:jaychou_lyrics.txt")
print("请重新运行你的 dataset_loader 代码。")
import math
def dataset_loader(file_path, batch_size):
"""
数据生成器:按照 batch_size 条 分批读取数据
"""
# 1. 读取文件所有行(文件不大时可以这样读,大文件可用 readline 逐行读)
with open(file_path, 'r', encoding='utf-8') as f:
data_lines = f.readlines()
# 2. 计算总行数和总批次
line_count = len(data_lines)
# math.ceil: 向上取整,例如 10条数据,每批3条,需要 4批
batch_count = math.ceil(line_count / batch_size)
# 3. 循环生成每一批次的数据
for i in range(batch_count):
# 切片:[0:8], [8:16], [16:24] ...
batch_data = data_lines[i * batch_size: i * batch_size + batch_size]
# 核心:用 yield 把每批次数据抛出,暂停等待下一次调用
yield batch_data
# 测试调用
if __name__ == '__main__':
# 创建生成器对象
my_generator = dataset_loader('./jaychou_lyrics.txt', 8)
# 每次调用 next(),只加载8条数据到内存中处理!
first_batch = next(my_generator)
print(f"第一批数据: {first_batch}")
# 或者用 for 循环遍历所有批次(内存中永远只有一期的数据)
# for batch in my_generator:
# print(f"处理批次: {len(batch)}条")

为什么这个设计很牛?
因为当文件有 5000 万行时,data_lines 虽然把所有行读进了内存,但如果是用文件指针逐行读取配合 yield,内存占用将永远只有 batch_size 那么大!这就是生成器“用时间换空间”的极致体现。
五、生成器进阶:send() 方法不仅能取,还能存!
除了 next() 唤醒生成器,Python 还提供了一个 send() 方法,它不仅能让生成器继续往下走,还能在暂停的地方给生成器传一个值!
def eat_dumplings():
print("准备吃饺子...")
while True:
# yield 返回饺子数量,同时接收外部 send() 传进来的新味道
flavor = yield "一个饺子"
print(f"吃到了 {flavor} 馅的饺子")
bob = eat_dumplings()
# 1. 第一次必须用 next() 或 send(None) 启动生成器,让它走到第一个 yield 处
next(bob) # 打印: 准备吃饺子... 并停在 yield 处
# 2. 使用 send() 传参,并获取这次的返回值
result = bob.send("猪肉大葱")
print(result) # 打印: 吃到了 猪肉大葱 馅的饺子 -> 返回: 一个饺子
result = bob.send("韭菜鸡蛋")
print(result) # 打印: 吃到了 韭菜鸡蛋 馅的饺子 -> 返回: 一个饺子

应用场景:在协程中,
send()是实现双向数据传输的核心机制。这也是为什么说生成器是Python协程的雏形。
六、总结与避坑指南
1. 对比一览表
| 特性 | 列表/字典等 | 迭代器 | 生成器 |
|---|---|---|---|
| 内存占用 | 大(一次性全装下) | 小(只记录位置) | 极小(边算边出) |
| 创建方式 | [], {} | iter(可迭代对象) | ()推导式, yield函数 |
| 访问方式 | 索引、切片、循环 | next(), 循环 | next(), send(), 循环 |
| 可重复遍历 | 可以(每次从头开始) | 不可以(单向道) | 不可以(单向道) |
2. 小白避坑指南
- 生成器是一次性的! 遍历完一次后,生成器就空了,不能像列表那样二次
for循环。如果还要用,只能重新创建一个生成器对象。 - 首次启动生成器:使用
send()传参前,必须先用next()或send(None)把生成器运行到第一个yield处,否则会报错TypeError: can't send non-None value to a just-started generator。 - 不要混淆 return 和 yield:函数里写了
yield就不能写return 返回值(只能写return表示结束)。因为生成器函数本身返回的已经是生成器对象了。
结语:迭代器和生成器是Python高阶语法的精华。理解了“懒加载”思想,你以后处理大数据集、写爬虫、做AI数据加载时,就能游刃有余,写出既优雅又省内存的代码!如果这篇博客对你有帮助,记得点赞+收藏哦!
14万+

被折叠的 条评论
为什么被折叠?



