迭代器 · 生成器

一、可迭代对象 vs 迭代器对象

1. 通俗类比

  • 可迭代对象(Iterable) = 一本可以翻的书
  • 迭代器(Iterator) = 一个会自己翻页的书签

书本身不会自己翻页,你得找个书签帮你翻。书签记得”我翻到第几页了”。

2. 可迭代对象(Iterable)

能被 for 循环遍历的东西,都是可迭代对象。

常见例子:

1
2
3
4
5
6
7
列表 [1, 2, 3]
字符串 "abc"
元组 (1, 2, 3)
字典 {"a": 1}
集合 {1, 2, 3}
range(5)
文件对象 open("a.txt")

判断方法:看有没有 __iter__() 方法。

1
2
nums = [1, 2, 3]
print(hasattr(nums, '__iter__')) # True

3. 迭代器对象(Iterator)

既能被 for 遍历,又能用 next() 一个一个取值的对象。

两个核心方法:

方法 作用
__iter__() 返回自己
__next__() 返回下一个值,取完抛 StopIteration
1
2
3
nums = [1, 2, 3]
it = iter(nums)
print(hasattr(it, '__next__')) # True

4. 体验

1
2
3
4
5
6
7
nums = [1, 2, 3]
it = iter(nums)

print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it)) # 抛异常 StopIteration

5. for 循环内部做了什么

1
2
for x in [1, 2, 3]:
print(x)

等价于:

1
2
3
4
5
6
7
it = iter([1, 2, 3])
while True:
try:
x = next(it)
print(x)
except StopIteration:
break

6. 两者关系

可迭代对象 ≠ 迭代器,但迭代器一定是可迭代对象。

7. 对比表

特性 可迭代对象 迭代器
能被 for 遍历 ✅ ✅
有 __iter__() ✅ ✅
有 __next__() ❌ ✅
能 next() 取值 ❌ ✅
记住遍历位置 ❌ ✅
能重复遍历 ✅ ❌(一次性)
例子 列表、字符串、字典 iter([1,2,3])、生成器

8. 迭代器是一次性的!!

1
2
3
4
5
nums = [1, 2, 3]
it = iter(nums)

print(list(it)) # [1, 2, 3]
print(list(it)) # [] ← 空了

9. 自己造一个

可迭代对象:

1
2
3
4
5
6
7
8
9
class MyList:
def __init__(self, data):
self.data = data

def __iter__(self):
return iter(self.data)

for x in MyList([1, 2, 3]):
print(x) # 1 2 3

迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter:
def __init__(self, n):
self.n = n
self.i = 0

def __iter__(self):
return self

def __next__(self):
if self.i >= self.n:
raise StopIteration
self.i += 1
return self.i

for x in Counter(3):
print(x) # 1 2 3

10. 总结

可迭代对象 = 能被遍历的东西(书)
迭代器 = 能被遍历 + 会自己往下走 + 一次性(书签)
可迭代是”资格”,迭代器是”能力”。


二、生成器(Generator)

1. 通俗类比

  • 普通函数 = 一次性把菜全做完端上来(列表)
  • 生成器 = 现做现上,你吃一口我做一道

2. 生成器是什么

生成器是一种特殊的迭代器。

特点:

  1. 用 yield 代替 return
  2. 不会一次性算完,而是”要一个给一个”(惰性求值)

一句话:生成器 = 会”暂停”的函数 + 自动成为迭代器。

3. 创建方式一:生成器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def make_gen():
yield 1
yield 2
yield 3

g = make_gen()
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
print(next(g)) # StopIteration

# 或用 for
for x in make_gen():
print(x) # 1 2 3

4. 创建方式二:生成器表达式

1
2
3
nums = (x * 2 for x in range(5))   # 圆括号
print(next(nums)) # 0
print(next(nums)) # 2

记忆点:

  • [] → 列表推导式 → 全部算完
  • () → 生成器表达式 → 用到才算

5. yield 到底做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def demo():
print("开始")
yield 1
print("继续")
yield 2
print("结束")

g = demo()
print("=== 还没执行 ===")
print(next(g))
print("=== 第一次暂停后 ===")
print(next(g))
print("=== 第二次暂停后 ===")
print(next(g))

输出:

1
2
3
4
5
6
7
8
9
=== 还没执行 ===
开始
1
=== 第一次暂停后 ===
继续
2
=== 第二次暂停后 ===
结束
StopIteration

yield = 暂停 + 返回值 + 记住现场。

6. 为什么用生成器:省内存

用列表(爆内存):

1
2
3
4
5
def make_nums():
result = []
for i in range(100000000):
result.append(i)
return result

用生成器(无压力):

1
2
3
4
5
6
def make_nums():
for i in range(100000000):
yield i

for n in make_nums():
print(n)

7. 经典例子:斐波那契

1
2
3
4
5
6
7
8
def fib_gen(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b

for x in fib_gen(10):
print(x) # 0 1 1 2 3 5 8 13 21 34

8. 两个常用方法

send():给生成器发值

1
2
3
4
5
6
7
8
9
10
def demo():
x = yield 1
print("收到:", x)
y = yield 2
print("收到:", y)

g = demo()
print(next(g)) # 1
print(g.send(100)) # 收到:100 → 2
print(g.send(200)) # 收到:200 → StopIteration

close():关闭生成器

1
2
3
g = demo()
next(g)
g.close()

9. 特点总结

特点 说明
用 yield 定义 有 yield 就是生成器函数
惰性求值 用到才算
一次性 用完就没了
自动是迭代器 能用 next() 和 for
状态保持 暂停后记住现场

10. 使用场景

场景 说明
数据量巨大 读 10G 日志,一行一行处理
无限序列 所有自然数
管道式处理 生成器套生成器
只要遍历一次 不需要保存全部数据

实战:读大文件

1
2
3
4
5
6
7
def read_big_file(path):
with open(path) as f:
for line in f:
yield line

for line in read_big_file("huge.log"):
process(line)

11. 对比表

对比项 列表 生成器
内存 全部存起来 只存当前
速度 全部算完才能用 边算边用
重复遍历 ✅ ❌
适用 数据小、反复用 数据大、只用一次

12. 总结

有 yield 就是生成器函数,圆括号推导式就是生成器表达式。
yield = 暂停 + 返回值 + 记住现场。
生成器 = 惰性求值 = 要一个算一个 = 省内存。


三、生成器的两种调用方式

1. 手动调用

基础:用 next()

1
2
3
4
5
6
7
8
9
10
def gen():
yield 1
yield 2
yield 3

g = gen()
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
print(next(g)) # StopIteration

半自动:用 for

1
2
for x in gen():
print(x)

真正的手动:用 send() 双向通信

1
2
3
4
5
6
7
8
9
def gen():
while True:
x = yield
print(f"收到:{x}")

g = gen()
next(g) # 先启动
g.send("你好") # 收到:你好
g.send("世界") # 收到:世界

典型场景:实时平均值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def averager():
total = 0
count = 0
avg = None
while True:
x = yield avg
total += x
count += 1
avg = total / count

g = averager()
next(g)
print(g.send(10)) # 10.0
print(g.send(20)) # 15.0
print(g.send(30)) # 20.0

2. 用装饰器调用

问题:用 send() 前必须先 next() 启动,容易忘。

1
2
g = averager()
g.send(10) # ❌ TypeError: can't send non-None value to a just-started generator

解决:自动启动装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import wraps

def auto_start(func):
@wraps(func)
def wrapper(*args, **kwargs):
g = func(*args, **kwargs)
next(g) # 自动启动
return g
return wrapper

@auto_start
def averager():
total = 0
count = 0
avg = None
while True:
x = yield avg
total += x
count += 1
avg = total / count

g = averager() # 自动启动
print(g.send(10)) # 10.0

官方例子:@contextmanager

1
2
3
4
5
6
7
8
9
10
11
12
from contextlib import contextmanager

@contextmanager
def my_open(path):
print("打开文件")
f = open(path)
yield f
f.close()
print("关闭文件")

with my_open("a.txt") as f:
print("使用文件")

3. 对比表

对比项 手动调用 装饰器调用
谁控制流程 你亲自 next/send 装饰器帮你启动/包装
启动步骤 必须手动 next(g) 自动完成
代码复杂度 灵活但要小心 简洁,逻辑清晰
典型用途 协程、双向通信 上下文管理器、自动启动

4. 总结

手动调用:你自己用 next() 和 send() 驱动生成器。
装饰器调用:装饰器帮你自动启动或包装生成器。
本质:都是”驱动生成器”,一个亲手,一个代劳。