生成器与迭代器
问题
Python 中迭代器协议是什么?生成器的原理和应用场景?yield 和 return 的区别?
答案
迭代器协议
任何实现了 __iter__ 和 __next__ 方法的对象都是迭代器:
class Counter:
"""自定义迭代器:从 start 数到 stop"""
def __init__(self, start: int, stop: int):
self.current = start
self.stop = stop
def __iter__(self):
return self # 迭代器返回自身
def __next__(self):
if self.current >= self.stop:
raise StopIteration # 迭代结束
value = self.current
self.current += 1
return value
for i in Counter(1, 4):
print(i) # 1, 2, 3
可迭代对象 vs 迭代器:
| 概念 | 协议 | 说明 |
|---|---|---|
| 可迭代(Iterable) | __iter__ | list, str, dict 等 |
| 迭代器(Iterator) | __iter__ + __next__ | 有状态,一次性消费 |
lst = [1, 2, 3] # 可迭代对象
it = iter(lst) # 获取迭代器
print(next(it)) # 1
print(next(it)) # 2
生成器函数
使用 yield 的函数会变成生成器函数,调用它返回一个生成器对象(特殊的迭代器):
def fibonacci():
"""无限斐波那契数列"""
a, b = 0, 1
while True:
yield a # 暂停执行,产出值
a, b = b, a + b
# 惰性求值:不会一次性生成所有值
fib = fibonacci()
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
# 取前 10 个
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
yield vs return:
| 对比 | yield | return |
|---|---|---|
| 函数类型 | 生成器函数 | 普通函数 |
| 执行方式 | 暂停并产出值,下次从暂停处继续 | 终止函数,返回值 |
| 返回值 | 生成器对象 | 普通值 |
| 内存 | 惰性求值,占用极少内存 | 一次性计算全部 |
生成器表达式
# 列表推导式 — 一次性创建所有元素
squares_list = [x**2 for x in range(1_000_000)] # 占用大量内存
# 生成器表达式 — 惰性求值
squares_gen = (x**2 for x in range(1_000_000)) # 几乎不占内存
# 常用于大数据量场景
total = sum(x**2 for x in range(1_000_000)) # 直接传给函数
send、throw 和 close
def accumulator():
"""接收外部发送的值"""
total = 0
while True:
value = yield total # yield 可以接收 send 的值
if value is None:
break
total += value
gen = accumulator()
next(gen) # 启动生成器,走到第一个 yield → 0
print(gen.send(10)) # 发送 10,total=10 → 10
print(gen.send(20)) # 发送 20,total=30 → 30
print(gen.send(5)) # 发送 5,total=35 → 35
yield from
yield from 委托给子生成器,简化嵌套迭代:
def flatten(nested):
"""递归展开嵌套列表"""
for item in nested:
if isinstance(item, list):
yield from flatten(item) # 委托给子生成器
else:
yield item
data = [1, [2, [3, 4]], 5, [6]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6]
实际应用场景
1. 读取大文件:
def read_large_file(file_path: str):
"""逐行读取大文件,不会一次性加载到内存"""
with open(file_path, "r") as f:
for line in f:
yield line.strip()
# 处理 10GB 的日志文件也不会 OOM
for line in read_large_file("huge_log.txt"):
if "ERROR" in line:
print(line)
2. 数据管道:
def read_csv(path):
for line in open(path):
yield line.strip().split(",")
def filter_active(rows):
for row in rows:
if row[2] == "active":
yield row
def extract_names(rows):
for row in rows:
yield row[0]
# 管道组合:数据流式处理,内存恒定
pipeline = extract_names(filter_active(read_csv("users.csv")))
for name in pipeline:
print(name)
常见面试问题
Q1: 生成器和普通函数的区别?
答案:
普通函数用 return 返回值并结束;生成器函数用 yield 暂停并产出值,保留执行状态。生成器是惰性求值的,适合处理大数据量或无限序列。
Q2: list(range(1000000)) 和 range(1000000) 的区别?
答案:
range(1000000)是一个惰性序列对象,不会立即生成所有数字,内存占用极小list(range(1000000))会立即创建包含 100 万个整数的列表,占用约 8MB 内存
Python 3 的 range 是惰性的(Python 2 的 range 返回列表,xrange 才是惰性的)。
Q3: 如何让一个对象可以用 for 循环遍历?
答案:
实现 __iter__ 方法,返回一个迭代器:
class NumberRange:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
# 方法一:返回生成器(推荐,简洁)
current = self.start
while current < self.end:
yield current
current += 1
for n in NumberRange(1, 5):
print(n) # 1, 2, 3, 4
Q4: 生成器的内存优势体现在哪里?
答案:
import sys
# 列表:一次性存储所有元素
lst = [i for i in range(1_000_000)]
print(sys.getsizeof(lst)) # ~8,000,056 bytes (约 8MB)
# 生成器:只保存执行状态
gen = (i for i in range(1_000_000))
print(sys.getsizeof(gen)) # ~200 bytes (约 200B)
内存差距在百万倍量级。生成器适合大文件处理、流式数据、无限序列等场景。
Q5: yield from 和普通 for + yield 有什么区别?
答案:
功能上等价,但 yield from 还能:
- 自动处理
send()、throw()、close()的转发 - 获取子生成器的返回值
def sub_gen():
yield 1
yield 2
return "done"
def main_gen():
result = yield from sub_gen() # 获取子生成器的 return 值
print(f"子生成器返回: {result}")
list(main_gen()) # [1, 2] + 打印 "子生成器返回: done"