引言
你有没有想过,for x in [1, 2, 3] 背后到底发生了什么?为什么列表、字典、字符串、文件对象都能用 for 循环遍历,而自定义的类却不行?为什么处理一个 10GB 的日志文件时,用 for line in f 内存占用恒定,而 f.readlines() 却会把内存撑爆?
这些问题的答案都指向同一个核心概念:迭代器(Iterator)。而在迭代器的基础上,Python 又提供了更优雅的语法糖——生成器(Generator),用 yield 关键字就能轻松实现惰性计算、无限序列和数据管道,让代码既简洁又高效。
本文将从 for 循环的底层原理讲起,系统梳理可迭代对象、迭代器、生成器的关系与区别,深入讲解 yield 的各种用法,并结合实际场景展示如何用生成器写出更优雅的 Python 代码。
一、可迭代对象与迭代器:for 循环的底层原理
1.1 什么是可迭代对象
可迭代对象(Iterable) 是实现了 __iter__() 方法(或 __getitem__() 方法)的对象,能够被 for 循环遍历。列表、元组、字符串、字典、集合、文件对象都是可迭代对象。
from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance("hello", Iterable)) # True
print(isinstance({"a": 1}, Iterable)) # True
print(isinstance(123, Iterable)) # False,整数不可迭代
1.2 什么是迭代器
迭代器(Iterator) 是实现了迭代器协议的对象,即同时实现了:
- __iter__():返回迭代器自身
- __next__():返回下一个元素,没有元素时抛出 StopIteration
迭代器是一个"数据流",只能往前遍历,不能回头,遍历完就空了。
from collections.abc import Iterator
lst = [1, 2, 3]
print(isinstance(lst, Iterator)) # False,列表是可迭代对象,不是迭代器
it = iter(lst) # 调用 iter() 获取迭代器
print(isinstance(it, Iterator)) # True
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# print(next(it)) # StopIteration,没有更多元素
1.3 for 循环的本质
for x in iterable 实际上做了两件事:
# for 循环
for x in [1, 2, 3]:
print(x)
# 等价于下面的手动实现
it = iter([1, 2, 3])
while True:
try:
x = next(it)
print(x)
except StopIteration:
break
理解了这个原理,你就明白为什么文件对象可以逐行读取大文件而不占内存——文件对象本身就是迭代器,next(f) 每次只读一行。
1.4 可迭代对象 vs 迭代器
| 必须实现 | __iter__() | __iter__() + __next__() |
| 能否多次遍历 | 可以(如列表) | 只能一次,遍历完就空 |
| 内存占用 | 通常存储全部元素 | 惰性计算,只存当前状态 |
| 例子 | list、str、dict、tuple | 文件对象、生成器 |
# 可迭代对象可以多次遍历
lst = [1, 2, 3]
print(list(lst)) # [1, 2, 3]
print(list(lst)) # [1, 2, 3],还能再遍历
# 迭代器只能遍历一次
it = iter([1, 2, 3])
print(list(it)) # [1, 2, 3]
print(list(it)) # [],已经空了
二、自定义迭代器:实现迭代器协议
要让自己的类支持 for 循环,只需实现 __iter__() 和 __next__():
class Countdown:
"""从 n 倒数到 1 的迭代器"""
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
# 使用
for num in Countdown(5):
print(num)
# 5 4 3 2 1
# 也可以手动调用
cd = Countdown(3)
print(next(cd)) # 3
print(next(cd)) # 2
print(next(cd)) # 1
自定义迭代器虽然强大,但需要维护状态(self.current)、写两个方法,比较繁琐。有没有更简单的方式?有——生成器。
三、生成器基础:yield 的魔法
3.1 什么是生成器
生成器(Generator) 是一种特殊的迭代器,用函数语法实现,不需要写 __iter__ 和 __next__,只需用 yield 关键字返回值。
生成器函数和普通函数的区别:
- 普通函数用 return 返回值,返回后函数结束;
- 生成器函数用 yield 返回值,返回后函数暂停,下次调用 next() 时从暂停处继续执行。
def countdown(start):
"""生成器版本的倒数"""
current = start
while current > 0:
yield current # 暂停并返回 current
current -= 1
# 调用生成器函数返回生成器对象,不会执行函数体
gen = countdown(5)
print(type(gen)) # <class 'generator'>
# 用 next() 驱动执行
print(next(gen)) # 5
print(next(gen)) # 4
print(next(gen)) # 3
# 也可以用 for 循环(生成器是迭代器)
for num in countdown(5):
print(num)
# 5 4 3 2 1
对比自定义迭代器类,生成器函数简洁了太多——状态自动保存在局部变量中,yield 自动处理暂停和恢复。
3.2 生成器的执行流程
def simple_gen():
print("开始执行")
yield 1
print("第一次 yield 之后")
yield 2
print("第二次 yield 之后")
yield 3
print("结束")
gen = simple_gen()
print("— 调用生成器函数后,函数体还没执行 —")
print(next(gen))
# 开始执行
# 1
print(next(gen))
# 第一次 yield 之后
# 2
print(next(gen))
# 第二次 yield 之后
# 3
# print(next(gen))
# 结束
# StopIteration
关键点:
- 调用生成器函数不会执行函数体,只返回生成器对象。
- 每次 next() 执行到 yield 处暂停,返回 yield 后面的值。
- 再次 next() 从暂停处继续,直到函数结束抛出 StopIteration。
3.3 生成器表达式
和列表推导式类似,生成器用圆括号创建生成器表达式:
# 列表推导式:一次性生成所有元素,占用内存
lst = [x ** 2 for x in range(1000000)]
print(type(lst)) # <class 'list'>
print(lst.__sizeof__()) # 占用大量内存
# 生成器表达式:惰性计算,不占内存
gen = (x ** 2 for x in range(1000000))
print(type(gen)) # <class 'generator'>
print(gen.__sizeof__()) # 几乎不占内存
# 使用时才计算
print(next(gen)) # 0
print(next(gen)) # 1
print(sum(gen)) # 可以直接传给 sum、list 等函数
生成器表达式和列表推导式语法几乎一样,只是把 [] 换成 ()。当数据量大、只需遍历一次时,优先用生成器表达式。
3.4 生成器的惰性计算优势
生成器最大的价值是惰性计算——只在需要时才生成下一个值,不提前存储所有数据。这使得生成器特别适合:
- 处理大数据集/大文件
- 表示无限序列
- 构建数据处理管道
# 无限斐波那契数列(用列表不可能实现,因为无限)
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 取前 10 个
fib = fibonacci()
for _ in range(10):
print(next(fib), end=" ")
# 0 1 1 2 3 5 8 13 21 34
四、生成器进阶:send、throw 与 yield from
4.1 send:向生成器发送数据
yield 不仅能向外输出数据,还能接收外部传入的数据。用 send(value) 可以把值发送给生成器,yield 表达式的返回值就是发送的值。
def echo_generator():
print("生成器启动")
while True:
received = yield # yield 在这里暂停,接收 send 传来的值
print(f"收到: {received}")
gen = echo_generator()
next(gen) # 必须先 next() 启动,执行到第一个 yield
# 生成器启动
gen.send("你好") # 收到: 你好
gen.send("世界") # 收到: 世界
gen.send(123) # 收到: 123
注意:第一次必须用 next(gen) 或 gen.send(None) 启动生成器,因为还没执行到 yield 处,无法接收值。
send 让生成器不仅能"拉取"数据,还能"推送"数据,这是协程(coroutine)的基础。
4.2 throw:向生成器抛出异常
throw(exc_type) 可以在生成器暂停处抛出一个异常,生成器可以捕获并处理:
def safe_generator():
while True:
try:
received = yield
print(f"处理: {received}")
except ValueError as e:
print(f"捕获到 ValueError: {e}")
gen = safe_generator()
next(gen)
gen.send("正常数据") # 处理: 正常数据
gen.throw(ValueError("出错了")) # 捕获到 ValueError: 出错了
gen.send("继续处理") # 处理: 继续处理,生成器还活着
4.3 close:关闭生成器
close() 可以主动关闭生成器,之后再 next() 会抛出 StopIteration:
gen = (x for x in range(5))
print(next(gen)) # 0
gen.close()
# print(next(gen)) # StopIteration
4.4 yield from:委托给子生成器
yield from 可以把一个生成器的迭代委托给另一个可迭代对象,简化嵌套生成器的写法:
# 不用 yield from
def nested_gen():
for x in [1, 2, 3]:
yield x
for x in (4, 5, 6):
yield x
print(list(nested_gen())) # [1, 2, 3, 4, 5, 6]
# 用 yield from,更简洁
def nested_gen2():
yield from [1, 2, 3]
yield from (4, 5, 6)
print(list(nested_gen2())) # [1, 2, 3, 4, 5, 6]
yield from 不只是语法糖,它还自动处理了 send、throw、close 在子生成器和调用者之间的转发,是构建复杂生成器管道和协程的重要工具。
# yield from 自动转发 send
def sub_gen():
while True:
x = yield
print(f"子生成器收到: {x}")
def main_gen():
yield from sub_gen() # 委托给子生成器
gen = main_gen()
next(gen)
gen.send("hello") # 子生成器收到: hello
五、实际应用场景
5.1 大文件逐行处理
def read_large_file(filepath):
"""逐行读取大文件,内存占用恒定"""
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# 处理 10GB 的日志文件
for line in read_large_file("huge.log"):
if "ERROR" in line:
process_error(line)
5.2 数据处理管道
生成器可以像 Unix 管道一样串联起来,每一步都是一个生成器,数据流式传递:
def read_lines(filepath):
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
yield line
def filter_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
def parse_lines(lines):
for line in lines:
parts = line.split("|")
yield {"time": parts[0], "level": parts[1], "msg": parts[2]}
# 构建管道:读文件 → 过滤错误 → 解析
lines = read_lines("app.log")
errors = filter_errors(lines)
records = parse_lines(errors)
# 消费数据
for record in records:
save_to_db(record)
整个管道是惰性的,只有在 for record in records 时才真正逐行处理,内存占用极低。
5.3 分页数据获取
def fetch_all_pages(api_url, page_size=100):
"""自动翻页获取所有数据"""
page = 1
while True:
data = requests.get(api_url, params={"page": page, "size": page_size}).json()
if not data["items"]:
break
for item in data["items"]:
yield item
page += 1
# 使用方不需要关心翻页逻辑
for item in fetch_all_pages("https://api.example.com/items"):
process(item)
5.4 生成器替代递归(避免栈溢出)
某些递归算法可以用生成器改写,避免递归深度过大导致栈溢出:
# 递归版二叉树中序遍历(深度大时栈溢出)
def inorder_recursive(node):
if node is None:
return
inorder_recursive(node.left)
yield node.value
inorder_recursive(node.right)
# 生成器 + 显式栈版(不会栈溢出)
def inorder_generator(root):
stack = []
node = root
while stack or node:
while node:
stack.append(node)
node = node.left
node = stack.pop()
yield node.value
node = node.right
六、常见坑点
坑点 1:生成器只能遍历一次
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [],已经空了!
# 如果需要多次遍历,用列表,或每次重新创建生成器
坑点 2:生成器表达式作为函数参数时括号可省略
# sum() 接受一个可迭代对象,生成器表达式的括号可以省略
print(sum(x ** 2 for x in range(10))) # 285
# 但如果有其他参数,不能省略
# print(sum(x ** 2 for x in range(10), 10)) # 语法错误
print(sum((x ** 2 for x in range(10)), 10)) # 正确,295
坑点 3:在生成器中用 return
生成器函数中的 return value 会终止生成器,value 会作为 StopIteration 的 value 属性,而不是被 next() 返回:
def gen_with_return():
yield 1
yield 2
return "结束了"
gen = gen_with_return()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration as e:
print(e.value) # 结束了,return 的值在这里
坑点 4:生成器不是列表,没有 len()、不能下标访问
gen = (x for x in range(10))
# len(gen) # TypeError,生成器没有长度
# gen[0] # TypeError,不能下标访问
# 只能迭代,或转成 list(gen) 后操作
坑点 5:闭包中的生成器延迟绑定
# 经典坑:生成器表达式中的变量延迟绑定
def create_generators():
return (lambda x: i * x for i in range(5))
gens = create_generators()
print([g(2) for g in gens]) # [8, 8, 8, 8, 8],不是 [0, 2, 4, 6, 8]!
# i 在生成器执行时才查找,此时 i 已经是 4
七、最佳实践总结
结语
迭代器是 Python for 循环的底层基石,生成器则是迭代器的优雅实现——用 yield 关键字,你就能以函数的简洁语法,获得惰性计算、无限序列、数据管道等强大能力。
理解了迭代器协议,你就能让自己的类支持 for 循环;掌握了生成器,你就能写出内存友好、逻辑清晰的数据流处理代码。而 send、throw、yield from 这些进阶特性,更是 Python 协程和异步编程的基础。
记住一句话:当你需要处理大量数据且只需遍历一次时,先想想能不能用生成器。

