迭代器与生成器:yield、生成器表达式
一句话概述
迭代器(Iterator)是实现了 __iter__ 和 __next__ 方法的对象,可以逐个产出元素而不需要一次性把所有数据加载到内存。生成器(Generator)则是 Python 最优雅的迭代器创建方式——用 yield 关键字的函数会自动变成生成器,每次 yield 暂停并返回值,下次调用从暂停点继续。生成器表达式 (x for x in ...) 则像列表推导式的"懒加载版本",按需产出元素。
💡 核心要点:①迭代器协议 =__iter__(返回自身)+__next__(返回下一个元素,耗尽抛 StopIteration)②yield让函数变成生成器——调用时不执行,每次next()执行到下一个yield③生成器是惰性求值(Lazy Evaluation),不像列表一次性在内存中创建所有元素 ④生成器表达式用圆括号语法(expr for var in iterable),适合简单场景 ⑤yield from可以委托子生成器,简化嵌套生成器写法
教学与演示
一、迭代器——可迭代对象背后的"搬运工"
是什么:迭代器是实现了迭代器协议的对象。这个协议只有两个方法:__iter__() 返回迭代器自身(通常就是 return self);__next__() 返回下一个元素,没有元素时抛出 StopIteration 异常。你平时写的 for x in something——Python 在背后先调用 iter(something) 获取迭代器,然后循环调用 next() 直到 StopIteration。
大白话 迭代器就像一个自动售货机——你投币(调用next()),它就吐出一个商品。商品吐完了,你再投币它就会亮红灯(抛出StopIteration)。你不需要知道机器内部有多少商品、怎么存放的——你只需要一次次投币就行。for循环就是一个自动投币的机器人——它一直投币直到红灯亮起。
为什么:迭代器的核心价值是惰性求值(Lazy Evaluation)——不需要一次性把所有数据加载到内存。处理一个 100GB 的日志文件时,如果用列表存储所有行,内存直接爆炸。但迭代器每次只读一行,处理完就丢弃,内存占用极小。Python 的内置类型 list、dict、str、tuple 都是可迭代对象(有 __iter__),但不是迭代器——它们每次调用 iter() 返回一个新的迭代器对象。
大白话 你想喝一杯水——列表的做法是把整个水库的水倒进你杯子里(内存爆炸),迭代器的做法是用吸管从水库里一口一口吸(内存友好)。对于大数据,吸管策略是唯一可行的方案。在 AI 中,训练图片数据集可能有几百万张——迭代器让你一次只加载一个 batch(比如 32 张),训练完就释放。
怎么做:
import numpy as np
# ====== 1. 理解迭代器协议 ======
# 任何 for 循环的背后都在调用 iter() 和 next()
# 列表是可迭代对象,但不是迭代器
numbers = [10, 20, 30, 40, 50]
print(f"numbers 是可迭代对象: {hasattr(numbers, '__iter__')}") # True
print(f"numbers 是迭代器吗: {hasattr(numbers, '__next__')}") # False!
# 通过 iter() 获取迭代器
it = iter(numbers) # 相当于 numbers.__iter__()
print(f"\nit 是迭代器: {hasattr(it, '__next__')}") # True
# 手动模拟 for 循环——每次 next() 取一个元素
print(f"next(it) = {next(it)}") # 10
print(f"next(it) = {next(it)}") # 20
print(f"next(it) = {next(it)}") # 30
print(f"next(it) = {next(it)}") # 40
print(f"next(it) = {next(it)}") # 50
# 迭代器耗尽后,next() 抛出 StopIteration
try:
next(it) # 已经没有元素了!
except StopIteration:
print("迭代器已耗尽,抛出 StopIteration!")
# ====== 2. 自定义迭代器类——实现协议 ======
class Countdown:
"""倒计时迭代器——从 start 倒数到 1"""
def __init__(self, start):
self.current = start + 1 # 初始值比 start 大 1
self.start = start
def __iter__(self):
"""返回迭代器自身——这让 Countdown 既是可迭代对象又是迭代器"""
return self
def __next__(self):
"""每次调用返回下一个倒数数字"""
self.current -= 1 # 递减
if self.current <= 0:
raise StopIteration # 到底了,停止
return self.current
print(f"\n=== 自定义倒计时迭代器 ===")
for num in Countdown(5): # for 循环自动调用 iter() 和 next()
print(f" 倒计时: {num}")
# 输出: 5, 4, 3, 2, 1
# ====== 3. 迭代器的重要特性:只能遍历一次 ======
print(f"\n=== 迭代器是一次性的 ===")
cd = Countdown(3)
print(f"第一次遍历: {list(cd)}") # [3, 2, 1]
print(f"第二次遍历: {list(cd)}") # [] —— 迭代器已经耗尽!
# 列表是可迭代对象,每次 iter() 返回新迭代器,所以可以多次遍历
data = [1, 2, 3]
print(f"列表第一次: {list(data)}") # [1, 2, 3]
print(f"列表第二次: {list(data)}") # [1, 2, 3] —— 可以反复遍历!
# ====== 4. AI 场景:模拟数据集迭代器 ======
class BatchIterator:
"""AI 数据集批量迭代器——每次产出一个 batch 的数据"""
def __init__(self, total_samples, batch_size):
self.total = total_samples # 总样本数
self.batch_size = batch_size # 每个 batch 的大小
self.current = 0 # 当前已取到的位置
np.random.seed(42) # 固定种子保证可复现
def __iter__(self):
return self
def __next__(self):
if self.current >= self.total:
raise StopIteration # 数据遍历完毕
# 计算当前 batch 的实际大小(最后一个 batch 可能不满)
actual_size = min(self.batch_size, self.total - self.current)
# 生成模拟数据(实际中是读取图片/文本)
batch_data = np.random.randn(actual_size, 5) # actual_size 个样本,每个 5 个特征
batch_labels = np.random.randint(0, 2, actual_size) # 二分类标签
self.current += actual_size
return batch_data, batch_labels
print(f"\n=== AI 批量数据迭代器 ===")
dataset = BatchIterator(total_samples=10, batch_size=4)
for epoch in range(1, 3): # 模拟 2 个 epoch
print(f"Epoch {epoch}:")
for batch_idx, (data, labels) in enumerate(BatchIterator(10, 4)):
print(f" Batch {batch_idx}: data shape={data.shape}, labels={labels}")什么用:在 AI/深度学习框架中,迭代器是数据加载的核心机制。PyTorch 的 DataLoader 本质上就是一个高级迭代器——它迭代产生 batch 数据,支持多线程预加载、shuffle、drop_last 等功能。TensorFlow 的 tf.data.Dataset 同样是基于迭代器模式。理解迭代器协议,你就能理解为什么 for batch in dataloader: 能自动遍历百万级数据集而不会内存溢出——因为它背后就是迭代器在按需加载。
二、生成器——用 yield 写迭代器的最简方式
是什么:生成器(Generator)是创建迭代器的最简单方式。如果一个函数中包含 yield 关键字,它就不再是普通函数,而是一个生成器函数。调用生成器函数不执行函数体,而是返回一个生成器对象。每次对这个生成器对象调用 next(),函数体执行到下一个 yield 语句,把 yield 后面的值返回出去,然后暂停——函数的状态(局部变量、执行位置)全部保留,下次 next() 从暂停点继续。
大白话 普通函数像一个一次性任务——你交代它做一件事,它从头做到尾,做完就忘了。生成器函数像一个"按暂停键"的游戏——你喊"开始"(next()),它玩到一个存档点(yield)就暂停,下次你喊"继续"(再次next()),它从上次存档点接着玩。每个yield就像一个传送带上的包裹——它把包裹放到传送带上(返回给你),然后等下一个包裹。
为什么:用类实现迭代器需要手写 __iter__、__next__、管理状态变量——代码臃肿。同样功能的生成器只需要一个函数 + yield,代码量减少 80%。更关键的是,生成器的"暂停-恢复"机制非常适合处理流式数据——读取大文件、接收网络流、处理无限序列(如实时传感器数据)。在 AI 中,生成器用于流式数据增强(每读一张图就变换一次,不缓存整张图)。
大白话 军训时教官喊"报数!"——如果用列表,全班 50 个人得先一起站好列队(一次性加载),然后挨个报数。如果用生成器——教官一次只叫一个人,那个人站起来报数后坐下,下一个人再起来。50 个人可以分批来——前面报完的可以走了,后面还没到的可以等。生成器就是"一次叫一个"的模式。
怎么做:
import numpy as np
# ====== 1. 最简单的生成器——yield 的基本用法 ======
def simple_generator():
"""包含 yield 的函数——这就是一个生成器函数!"""
print(" → 生成器开始执行...")
yield 1 # 第一次调用 next() 执行到这里,返回 1,然后暂停
print(" → 第一次暂停后继续...")
yield 2 # 第二次调用 next() 从这里继续,返回 2
print(" → 第二次暂停后继续...")
yield 3 # 第三次调用 next() 从这里继续,返回 3
print(" → 生成器函数结束") # 第四次调用 next() 抛出 StopIteration
print("=== 生成器基本行为 ===")
gen = simple_generator() # 调用生成器函数——注意!函数体还没执行!
print(f"gen 的类型: {type(gen)}") # <class 'generator'>
print(f"gen 是迭代器: {hasattr(gen, '__next__')}") # True
print(f"\nnext(gen) = {next(gen)}") # 执行到第一个 yield,返回 1
print(f"next(gen) = {next(gen)}") # 从上次暂停处继续,返回 2
print(f"next(gen) = {next(gen)}") # 返回 3
# next(gen) ❌ StopIteration —— 生成器执行完毕
# ====== 2. 生成器保存局部状态——这是关键! ======
def counter_generator(limit):
"""计数器生成器——yield 之间保留 count 的值"""
count = 1
while count <= limit:
yield count # 返回 count 值,暂停在这里
count += 1 # 下次 next() 从这里继续!count 的值保留了!
# while 循环结束,生成器自动抛出 StopIteration
print(f"\n=== 生成器保留状态 ===")
for num in counter_generator(5):
print(f" {num}", end=" ")
print() # 1 2 3 4 5
# ====== 3. 生成器表达式——一行搞定简单生成器 ======
# 生成器表达式用 () 而不是 []
# 列表推导式:创建完整的列表(一次性占内存)
list_comp = [x ** 2 for x in range(10)]
print(f"\n列表推导式: {list_comp}") # [0, 1, 4, 9, ..., 81]
print(f" 内存占用: 整个列表同时存在")
# 生成器表达式:创建一个生成器(懒加载)
gen_expr = (x ** 2 for x in range(10))
print(f"生成器表达式: {gen_expr}") # <generator object>
print(f" 按需取值: {next(gen_expr)}") # 0
print(f" 按需取值: {next(gen_expr)}") # 1
print(f" 剩余全部: {list(gen_expr)}") # [4, 9, 16, ..., 81]
# ====== 4. AI 场景:无限数据生成器(数据增强) ======
def infinite_data_stream():
"""模拟无限的数据流——生成器不会自己停止"""
np.random.seed(123)
while True: # 无限循环!
# 每次生成一个随机样本——模拟实时数据流
x = np.random.uniform(-1, 1, (1, 3)) # 3 个特征的样本
y = np.sum(x) + np.random.normal(0, 0.1) # 标签 = 特征和 + 噪声
yield x, y # 产出一个样本,暂停
print(f"\n=== 无限数据流生成器 ===")
stream = infinite_data_stream()
# 取 5 个样本——生成器可以无限取,但每次只计算一个
for i in range(5):
x, y = next(stream)
print(f" 样本{i+1}: x={np.round(x, 3).ravel()}, y={y:.3f}")
print("只要继续 next(),生成器可以无限产生数据!")
# ====== 5. AI 场景:流式数据增强管道 ======
def data_augmentation_pipeline(raw_generator):
"""
数据增强管道——接收原始数据生成器,逐样本做增强
每个样本通过 y=yield x 接收和产出,形成管道
"""
for data, label in raw_generator:
# 增强1:添加随机噪声
data = data + np.random.normal(0, 0.05, data.shape)
# 增强2:随机缩放(0.9~1.1 倍)
scale = np.random.uniform(0.9, 1.1)
data = data * scale
yield data, label # 产出增强后的样本
print(f"\n=== 数据增强管道 ===")
raw_stream = infinite_data_stream()
augmented_stream = data_augmentation_pipeline(raw_stream)
for i in range(3):
x, y = next(augmented_stream)
print(f" 增强样本{i+1}: x={np.round(x, 3).ravel()}, y={y:.3f}")
# 每个样本从生成到增强都在一条流水线上按需完成——不占内存!什么用:生成器是 AI 数据处理管道的核心模式。当你用 PyTorch 的 DataLoader 时,每次 for batch in dataloader: 背后就是生成器在工作。图像数据增强(旋转、翻转、色彩抖动)通常也用生成器管道实现——原始图片 → 随机裁剪 → 归一化 → 转换为 Tensor,每一步都是生成器的一个环节,数据像流水线一样"流"过去,不缓存中间结果。Keras 的 ImageDataGenerator 本质就是一个强大的生成器。处理大规模文本语料时,生成器让你可以逐行读取 TB 级别的文件而不崩溃。
三、yield from——让生成器之间"打电话"
是什么:yield from 是 Python 3.3+ 引入的语法,用于在生成器中委托另一个迭代器产生值。yield from iterable 等价于 for item in iterable: yield item,但更简洁且性能更好。它不仅转发 yield(产出值),还能转发 next()、send()、throw()、close() 等操作——建立了双向通道。
大白话 普通yield是你自己亲手把东西递给别人。yield from是你叫了一个"快递员"——你告诉快递员"去那个仓库取东西,取到了直接给客户"。快递员(子生成器)帮你跑腿,你(父生成器)在旁边等着就行。快递员取完货了,你还可以继续做自己的事。
为什么:当你有嵌套的数据结构需要展平(flatten)时,yield from 让代码简洁一个数量级。更关键的是,yield from 不仅转发产出值——它还建立了完整的生成器委派协议。子生成器的返回值可以通过 yield from 赋给父生成器的变量,这在协程编程中非常重要。
怎么做:
import numpy as np
# ====== 1. 没有 yield from 时——需要嵌套 for 循环 ======
def flatten_old(nested_list):
"""老办法:展平嵌套列表——需要用 for 循环"""
for sublist in nested_list:
for item in sublist:
yield item # 两层循环,代码冗长
# ====== 2. 用 yield from——一行替代内层循环 ======
def flatten_new(nested_list):
"""新办法:用 yield from 替代内层循环"""
for sublist in nested_list:
yield from sublist # 等价于 for item in sublist: yield item
print("=== yield from 对比 ===")
data = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
print(f"嵌套数据: {data}")
print(f"老办法展平: {list(flatten_old(data))}") # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(f"新办法展平: {list(flatten_new(data))}") # 同上
# ====== 3. yield from 可以连接多个生成器——数据管道 ======
def text_reader(file_names):
"""读取多个文本文件——每个文件名产出一行(模拟)"""
for fname in file_names:
# 模拟读取文件内容
for line in range(1, 4): # 假设每个文件有 3 行
yield f"[{fname}] 第{line}行数据"
def tokenize(text_generator):
"""分词器——把文本行拆分成单词(模拟)"""
for text in text_generator:
# 模拟分词:按空格拆分
words = text.split()
yield from words # 逐个产出单词,而不是整个列表
print(f"\n=== yield from 数据管道 ===")
files = ["data_a.txt", "data_b.txt"]
text_stream = text_reader(files)
word_stream = tokenize(text_stream)
print(f"最终产出的单词流: {list(word_stream)}")
# ====== 4. AI 场景:多文件数据集的统一生成器 ======
def multi_file_image_loader(file_list):
"""
加载多个图像文件——用 yield from 汇总多个子迭代器
实际项目中:file_list 包含成百上千个 TFRecord/LMDB 文件
"""
for file_name in file_list:
# 模拟每个文件包含若干样本
yield from single_file_loader(file_name) # 依次产出每个文件中的样本
def single_file_loader(file_name):
"""模拟从单个文件加载样本"""
# 假设每文件有 2 个样本
num_samples = 2
for i in range(num_samples):
# 模拟一个样本:图像 + 标签
image = np.random.randn(3, 32, 32) # 3 通道 32x32 图像
label = np.random.randint(0, 10) # 0-9 分类标签
# 产出样本文件名 + 样本编号 + (图像, 标签)
yield f"{file_name}_sample{i}", (image, label)
print(f"\n=== 多文件数据加载器 ===")
file_list = [f"train_{str(i).zfill(4)}.tfrecord" for i in range(1, 4)]
loader = multi_file_image_loader(file_list)
# 遍历所有文件的所有样本
sample_count = 0
for sample_id, (img, lbl) in loader:
sample_count += 1
print(f" {sample_id}: image shape={img.shape}, label={lbl}")
if sample_count >= 6: # 演示只取前 6 个
break
print(f"\n共加载 {sample_count} 个样本——"
f"背后是 {len(file_list)} 个文件,yield from 无缝衔接!")什么用:在 AI 数据处理中,yield from 用于构建复杂的数据管道。一个典型场景:训练前需要合并多个数据源——图文数据、纯文本数据、问答数据,来自不同格式的文件(TFRecord、LMDB、CSV)。yield from 可以把这些不同来源的生成器无缝拼接成一个统一的训练数据流。TensorFlow 的 tf.data.Dataset.interleave() 内部逻辑和 yield from 类似。另一个场景是递归遍历目录结构——遍历文件夹及其所有子文件夹中的图片——用生成器 + yield from 可以高效展平目录树。
四、send() 与生成器的高级交互
是什么:生成器不仅是"单向产出"——它还支持 send() 方法,可以向生成器内部发送值。send(value) 的行为是:从上次暂停点恢复执行,并且让 yield 表达式的值等于 send() 的参数。生成器启动时只能用 send(None) 或 next()(两者等价)。此外还有 throw()(向生成器内部抛异常)和 close()(终止生成器)。
大白话 普通的yield是生成器往外递东西——像快递员给你送货。send()反过来——你往生成器里面塞东西。就像你和生成器之间建立了一条双向通道:生成器通过yield把东西递给你,你通过send()把东西递还给生成器。这个机制让生成器变成了一个"协程"——可以双向通信的函数。
为什么:send() 让生成器从"只读数据源"升级为"可交互的状态机"。在 AI 中,send() 可以用于实现训练循环中的动态策略调整——比如训练过程中根据损失值动态调整数据增强的强度,或者根据验证集准确率调整学习率调度策略。
怎么做:
import numpy as np
# ====== 1. send() 的基本用法——双向通信 ======
def echo_generator():
"""接收值的生成器——yield 既是产出也是接收"""
print(" 生成器启动!")
# 第一个 yield:产出初始值,然后等待 send()
received = yield "准备好了,请发送数据"
print(f" 收到: {received}")
# 第二个 yield:把收到的值加倍返回,继续等待
received = yield f"收到了「{received}」,请继续发送"
print(f" 再次收到: {received}")
yield f"最后一次收到「{received}」,生成器即将结束"
print(" 生成器结束!")
print("=== send() 双向通信 ===")
gen = echo_generator()
# 第一步:必须用 send(None) 或 next() 启动生成器
first_response = gen.send(None) # 等价于 next(gen)
print(f"生成器产出: {first_response}")
# 第二步:发送值并接收响应
second_response = gen.send("Hello Python!")
print(f"生成器产出: {second_response}")
# 第三步:再次发送
third_response = gen.send("Bye!")
print(f"生成器产出: {third_response}")
# ====== 2. AI 场景:动态调整训练超参数的协程 ======
def dynamic_learning_rate_scheduler(initial_lr=0.1, decay_rate=0.95):
"""
学习率调度器协程——根据外部传入的验证损失动态调整学习率
外部通过 send(val_loss) 传入验证损失,生成器 yield 新的学习率
"""
lr = initial_lr
best_loss = float("inf")
patience_counter = 0
while True:
# yield 当前学习率,同时接收外部传入的 val_loss
val_loss = yield lr
if val_loss is None: # 启动时 send(None),跳过判断
continue
if val_loss < best_loss:
best_loss = val_loss
patience_counter = 0 # 损失改善,重置耐心计数
else:
patience_counter += 1
# 如果连续 3 次没改善,降低学习率
if patience_counter >= 3:
lr *= decay_rate # 学习率衰减
patience_counter = 0
print(f" [调度器] 学习率衰减至: {lr:.5f}")
print(f"\n=== 动态学习率调度器 ===")
scheduler = dynamic_learning_rate_scheduler(initial_lr=0.1)
# 启动协程
initial_lr = scheduler.send(None)
print(f"初始学习率: {initial_lr:.4f}")
# 模拟训练:传入验证损失,获取调整后的学习率
val_losses = [0.85, 0.72, 0.70, 0.69, 0.71, 0.71, 0.72, 0.70]
for epoch, loss in enumerate(val_losses):
new_lr = scheduler.send(loss) # 传入损失,获取新学习率
print(f" Epoch {epoch+1}: val_loss={loss:.2f} → lr={new_lr:.4f}")
scheduler.close() # 关闭协程
# ====== 3. close() 和 throw() —— 控制生成器生命周期 ======
def controlled_generator():
"""演示 close() 和 throw() 的生成器"""
try:
for i in range(5):
yield f"正常产出: {i}"
except GeneratorExit:
print(" [生成器被 close() 关闭]")
except ValueError as e:
print(f" [生成器收到 ValueError: {e}]")
yield "处理异常后继续"
print(f"\n=== close() 和 throw() ===")
cg = controlled_generator()
print(f" {next(cg)}") # 正常产出: 0
cg.close() # 强制关闭生成器(在 yield 处引发 GeneratorExit)
# print(next(cg)) ❌ StopIteration —— 已关闭
# 重新创建演示 throw()
cg2 = controlled_generator()
print(f" {next(cg2)}") # 正常产出: 0
try:
cg2.throw(ValueError, "传入一个错误") # 向生成器内部抛异常
except StopIteration:
pass # 生成器已被 throw 终止什么用:在高级 AI 场景中,生成器的协程能力可以实现复杂的训练控制逻辑。比如 GAN 训练中的交替训练——生成器和判别器的训练可以用协程相互协调。分布式训练中的参数服务器——send() 用于接收梯度更新请求,yield 返回参数。虽然现代深度学习框架封装了很多细节,但理解协程机制能帮你理解框架底层的工作原理和编写自定义训练循环。
概念关系图谱
| 概念 | 核心含义 | 与AI的关系 | 关联概念 |
|---|---|---|---|
| 迭代器 | 实现 __iter__/__next__ 的对象 | DataLoader 迭代批量数据 | 可迭代对象、StopIteration |
| 可迭代对象 | 实现了 __iter__ 的对象 | list、dict、Dataset 都是可迭代对象 | 迭代器、iter() |
| 生成器 | 含 yield 的函数返回的对象 | 数据增强管道、流式加载 | yield、惰性求值 |
| yield | 生成器暂停并返回值的关键字 | 数据流中产出每个 batch | send()、next() |
| yield from | 委托子生成器产生值 | 多数据源合并、递归目录遍历 | yield、迭代器 |
| 惰性求值 | 按需计算,不一次性加载 | TB 级数据处理、无限数据流 | 生成器、迭代器 |
| 生成器表达式 | (expr for x in it) 一行创建生成器 | 简单数据转换过滤器 | 列表推导式、惰性求值 |
| send() | 向生成器内部发送值 | 动态调整训练超参数 | yield、协程 |
重点答疑
Q1: 迭代器和可迭代对象有什么区别?
可迭代对象(Iterable)是实现了 __iter__ 方法的对象——list、dict、str、tuple 都是可迭代对象,但它们不是迭代器。迭代器(Iterator)同时实现了 __iter__ 和 __next__ 方法。关键区别:可迭代对象每次调用 iter() 返回一个全新的迭代器(所以能多次遍历);迭代器本身是可迭代的(iter(iterator) 返回自身),但遍历一次就耗尽了。简单记法:可迭代对象是"有数据的东西",迭代器是"帮你取数据的人"。在 AI 中,torch.utils.data.Dataset 是可迭代对象,DataLoader 是迭代器。
Q2: 什么时候用生成器表达式,什么时候用生成器函数?
生成器表达式适合简单的一行转换:(x*2 for x in data)。生成器函数适合包含复杂逻辑(条件判断、多步处理、状态维护)的场景。判断标准:如果你能用一个表达式写完逻辑,用生成器表达式;如果需要写多行代码、多个 if 分支、try/except,用生成器函数。在 AI 中,数据预处理通常用生成器函数(因为需要多步变换),简单的数据过滤可以用生成器表达式。
Q3: yield from 和 for item in x: yield item 真的一样吗?
不完全一样。yield from 不仅仅是语法糖——它还建立了完整的双向通道。yield from subgen 会透传 send()、throw()、close() 操作给子生成器,而 for item in x: yield item 只能转发 next()。yield from 还能捕获子生成器的 return 返回值并赋给父生成器的变量:result = yield from subgen()。在协程编程中,这个区别至关重要。
Q4: 怎样判断一个对象是生成器还是普通迭代器?
最可靠的方法是用 import types; isinstance(obj, types.GeneratorType)。直观方法:用 inspect.isgenerator()。经验法则:如果是用 yield 函数或生成器表达式 (...) 创建的,就是生成器;如果是用类实现的 __next__,就是普通迭代器。生成器是迭代器的一种特殊类型——所有生成器都是迭代器,但不是所有迭代器都是生成器。
章节单词汇总
| 英文 | 音标 | 术语/释义 |
|---|---|---|
| iterator | /ɪtəˈreɪtər/ | 迭代器;实现 __next__ 的对象,每次返回一个元素 |
| generator | /ˈdʒenəreɪtər/ | 生成器;用 yield 创建的特殊迭代器 |
| yield | /jiːld/ | 产出;生成器中暂停并返回值的关键字 |
| iterable | /ˈɪtərəbəl/ | 可迭代的;实现了 __iter__ 的对象 |
| lazy evaluation | /ˈleɪzi ɪˌvæljuˈeɪʃən/ | 惰性求值;按需计算,不提前生成所有值 |
| StopIteration | /stɑːp ɪtəˈreɪʃən/ | 停止迭代;迭代器耗尽时抛出的异常 |
| coroutine | /ˈkoʊruːtiːn/ | 协程;可双向通信的生成器(支持 send) |
| pipeline | /ˈpaɪplaɪn/ | 管道;多个生成器串联形成的数据处理流水线 |
面试练习
Q1 [单选] 以下哪个关于生成器的说法是正确的?
- A. 生成器是一次性把所有数据加载到内存
- B. 包含
yield关键字的函数调用后返回生成器对象 - C. 生成器不能被
for循环遍历 - D. 生成器表达式使用中括号
[...]
解答:B 正确。包含yield的函数是生成器函数,调用后返回生成器对象而不是执行函数体。A 错误——生成器是惰性求值,不一次性加载。C 错误——生成器是可迭代对象,可以被for遍历。D 错误——生成器表达式用圆括号(...),中括号是列表推导式。
Q2 [单选] 迭代器耗尽后调用 next() 会发生什么?
- A. 返回
None - B. 返回最后一个元素
- C. 抛出
StopIteration异常 - D. 重新从头开始
解答:C 正确。迭代器耗尽后,next()抛出StopIteration异常——这是迭代器协议的规定,for循环也是靠这个异常来判断何时结束遍历。迭代器不会自动重置,不能"从头再来"。
Q3 [多选] 关于 yield from 的说法正确的是?
- A.
yield from iterable等价于for item in iterable: yield item(基础功能) - B.
yield from可以建立双向通道,透传send()、throw()、close() - C.
yield from只能用于生成器,不能用于普通迭代器 - D. 可以用
result = yield from subgen()获取子生成器的返回值
解答:A、B、D 正确。C 错误——yield from可以用于任何可迭代对象(list、set、dict、生成器等),不限于生成器。D 是yield from的重要特性——子生成器通过return返回的值可以被父生成器捕获。
Q4 [单选] 以下代码输出什么?gen = (x*x for x in range(3)); print(list(gen)); print(list(gen))
- A.
[0, 1, 4]再[0, 1, 4] - B.
[0, 1, 4]再[] - C. 报错
- D.
[0, 1, 4]再None
解答:B 正确。生成器(包括生成器表达式)是迭代器,只能遍历一次。第一次list(gen)消耗了生成器的所有元素,第二次list(gen)时生成器已经耗尽,返回空列表。如果想多次遍历,用列表推导式[x*x for x in range(3)]。
Q5 [多选] 关于生成器的 send() 方法,正确的是?
- A. 第一次调用生成器时,必须用
send(None)或next() - B.
send(value)会让yield表达式的值为value - C.
send()和next()完全等价 - D.
send()同时完成"接收上一个 yield 的返回值"和"发送值到生成器内"
解答:A、B、D 正确。C 错误——next()等价于send(None),但不能传递非 None 的值。send()比next()多了发送值给生成器的功能。生成器刚创建时没有yield表达式在等待,所以第一次必须传None。
Q6 [单选] 以下生成器函数中 send() 的行为是?def g(): x = yield; print(x); yield; g = g(); g.send(None); g.send('hello')
- A. 打印
None,然后抛出 StopIteration - B. 打印
hello,然后正常暂停 - C. 报错
- D. 打印
None,然后正常暂停
解答:B 正确。执行流程:g.send(None)启动生成器,执行到yield(右边没有值),返回None给调用者。g.send('hello')从上次yield处恢复,并且x = yield表达式的值变为'hello'(因为send传入了'hello')。接着print(x)打印hello,遇到第二个yield暂停。
Q7 [单选] 在 Python 中,以下哪个不是可迭代对象?
- A. 字符串
"hello" - B. 列表
[1, 2, 3] - C.
range(10) - D. 整数
42
解答:D 正确。整数没有实现__iter__方法,不是可迭代对象。字符串、列表、range对象都是可迭代对象。可以用hasattr(obj, '__iter__')来检查。
Q8 [单选] Python 中 for item in obj: 的执行流程是?
- A. 直接调用
obj.next()直到返回None - B. 调用
obj.__iter__(),然后对返回的迭代器调用__next__()直到StopIteration - C. 调用
iter(obj),然后循环next()直到StopIteration - D. 把
obj转换成列表再遍历
解答:C 正确。for循环首先调用iter(obj)(等价于obj.__iter__())获取迭代器,然后在循环中反复调用next(iterator)(等价于iterator.__next__()),捕获到StopIteration时停止循环。这就是为什么只要对象实现了__iter__就能被for遍历。
Q9 [多选] 以下哪些场景适合使用生成器而非列表?
- A. 处理一个大日志文件(几十 GB),逐行分析
- B. 实时传感器数据流,持续产生数据
- C. 数据增强管道,每个样本需要多步变换
- D. 需要多次遍历同一个序列
解答:A、B、C 正确——这些都是惰性求值和按需生成的优势场景。D 错误——如果需要多次遍历同一个序列,应该用列表或其他可重复遍历的数据结构。生成器是一次性的,遍历一次后就耗尽。
Q10 [多选] 关于生成器表达式和列表推导式,正确的是?
- A. 生成器表达式用
(),列表推导式用[] - B. 生成器表达式是惰性求值,列表推导式是立即求值
- C.
sum(x*x for x in range(1000000))比sum([x*x for x in range(1000000)])内存占用更小 - D. 生成器表达式访问速度比列表推导式更快(索引访问)
解答:A、B、C 正确。C 中生成器版本不需要创建包含 100 万个元素的中间列表,内存友好。D 错误——生成器不支持索引访问(不能gen[5]),因为它是惰性的,需要逐个产生。列表支持 O(1) 的索引访问。sum()内置函数直接传生成器表达式时,甚至可以省略内层括号。