写在最前
我们每天都在使用 Python 的迭代机制:遍历列表、逐行读取文件、处理查询结果,甚至在推导式中生成新数据。看起来简单的 for 循环,背后其实由“可迭代对象”和“迭代器”共同完成。
生成器则是在这套机制上提供的一种便捷写法。它可以按需产生数据,而不必一次把全部结果存进内存。
理解这几个概念之后,很多 Python 代码都会变得更加自然。
1. 先认识三个概念
在正式写代码前,先区分三个容易混淆的概念。
1.1 可迭代对象
能够被 for 循环遍历的对象,叫作可迭代对象(iterable)。
常见的可迭代对象包括:
- 列表;
- 元组;
- 字符串;
- 字典;
- 集合;
range对象;- 文件对象。
例如,列表可以直接交给 for 循环:
languages = ["Python", "Rust", "Go"]
for language in languages:
print(language)输出:
Python
Rust
Go1.2 迭代器
迭代器(iterator)是负责“记住当前位置,并返回下一个值”的对象。
它支持两个关键操作:
iter(object) # 获得迭代器
next(iterator) # 取得下一个值1.3 生成器
生成器(generator)是一种特殊的迭代器。它通常由包含 yield 的生成器函数,或者生成器表达式创建。
三者之间的关系可以概括为:
可迭代对象
│
│ iter()
▼
迭代器
│
├── next() ──> 下一个值
├── next() ──> 下一个值
└── next() ──> StopIteration
生成器是迭代器的一种便捷实现2. 使用 iter() 和 next()
下面不使用 for 循环,手动遍历一个播放列表:
playlist = ["Track A", "Track B", "Track C"]
player = iter(playlist)
print(next(player))
print(next(player))
print(next(player))输出:
Track A
Track B
Track Cplayer 会记住已经读取到哪里。每调用一次 next(),位置就向前移动一次。
如果所有元素都已经取完,再次调用 next():
print(next(player))程序会抛出异常:
StopIterationStopIteration 并不表示程序出现了意外故障,它是迭代器发出的正常结束信号。
3. for 循环背后做了什么
我们平时写的代码是:
colors = ["blue", "white", "black"]
for color in colors:
print(color)从迭代协议的角度看,它大致相当于:
colors = ["blue", "white", "black"]
iterator = iter(colors)
while True:
try:
color = next(iterator)
print(color)
except StopIteration:
breakfor 循环替我们完成了四件事:
- 调用
iter()获取迭代器; - 反复调用
next(); - 把得到的值赋给循环变量;
- 捕获
StopIteration并结束循环。
所以日常代码中没有必要手动捕获 StopIteration,直接使用 for 通常更加清晰。
4. 可迭代对象不一定是迭代器
列表是可迭代对象,但列表本身不是迭代器:
numbers = [10, 20, 30]
print(iter(numbers) is numbers)输出:
Falseiter(numbers) 创建了一个单独的列表迭代器。
而迭代器调用 iter() 时,通常会返回自己:
numbers = [10, 20, 30]
iterator = iter(numbers)
print(iter(iterator) is iterator)输出:
True这个差别非常重要:列表可以反复开始新的遍历,而一个已经消耗过的迭代器不会自动回到开头。
numbers = [10, 20, 30]
iterator = iter(numbers)
print(list(iterator))
print(list(iterator))输出:
[10, 20, 30]
[]第一次转换已经取完所有元素,因此第二次只能得到空列表。
5. 创建自己的迭代器
一个对象想成为迭代器,需要实现两个特殊方法:
__iter__():返回迭代器对象;__next__():返回下一个值,并在结束时抛出StopIteration。
下面创建一个按照指定步长递增的迭代器:
class StepCounter:
def __init__(self, start, stop, step):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
counter = StepCounter(start=2, stop=11, step=3)
for number in counter:
print(number)输出:
2
5
8执行过程如下:
for调用counter.__iter__();- 每轮调用一次
counter.__next__(); - 当前值达到
stop后抛出StopIteration; for循环收到结束信号并停止。
手动实现迭代器能帮助我们理解原理,但很多时候使用生成器会更简单。
6. 使用 yield 创建生成器
只要函数体中出现 yield,这个函数就成为生成器函数。
下面用生成器逐个产生分页编号:
def page_numbers(total_pages):
page = 1
while page <= total_pages:
yield page
page += 1
pages = page_numbers(4)
print(pages)输出类似:
<generator object page_numbers at 0x...>调用生成器函数时,函数体不会立刻完整执行,而是先返回一个生成器对象。
可以使用 next() 逐个取值:
pages = page_numbers(4)
print(next(pages))
print(next(pages))输出:
1
2也可以直接使用 for:
for page in page_numbers(4):
print(page)输出:
1
2
3
47. yield 为什么能够“暂停”函数
普通函数遇到 return 后会结束,并丢弃内部状态:
def get_status():
return "ready"生成器函数遇到 yield 时,会完成三件事:
- 返回
yield后面的值; - 暂停在当前位置;
- 保存局部变量和执行状态。
下一次调用 next() 时,它会从暂停的位置继续运行。
def process_steps():
print("开始准备")
yield "prepare"
print("开始执行")
yield "run"
print("开始收尾")
yield "finish"
steps = process_steps()
print(next(steps))
print(next(steps))
print(next(steps))输出:
开始准备
prepare
开始执行
run
开始收尾
finish每次 next() 只会让函数运行到下一个 yield,而不是从头重新执行。
当函数执行到末尾,或者遇到不带值的 return,Python 会自动产生 StopIteration。
8. 生成器为什么节省内存
假设需要处理一百万个整数。列表推导式会先创建并保存全部结果:
squares_list = [number ** 2 for number in range(1_000_000)]生成器表达式则只描述计算规则:
squares_generator = (number ** 2 for number in range(1_000_000))只有在取出某个值时,它才进行对应计算:
print(next(squares_generator))
print(next(squares_generator))
print(next(squares_generator))输出:
0
1
4两种方式的差别可以简单理解为:
列表:先把所有结果准备好,再交给使用者
生成器:使用者需要一个,就现场生成一个生成器通常更节省内存,但它也有代价:数据一般只能顺序读取一次,不能像列表一样随时通过下标访问。
9. 生成器表达式
生成器表达式的写法与列表推导式很相似,只是把方括号换成圆括号:
# 列表推导式
even_list = [number for number in range(10) if number % 2 == 0]
# 生成器表达式
even_generator = (number for number in range(10) if number % 2 == 0)查看结果:
print(even_list)
print(list(even_generator))输出:
[0, 2, 4, 6, 8]
[0, 2, 4, 6, 8]最终数值相同,但产生结果的时机不同。
生成器表达式作为函数的唯一参数时,可以省略额外的一层圆括号:
total = sum(number ** 2 for number in range(1, 6))
print(total)输出:
5510. 一个实际例子:分批处理数据
假设系统中有一组任务,我们希望每次只取固定数量,分批交给后续程序处理:
def split_batches(items, batch_size):
for start in range(0, len(items), batch_size):
end = start + batch_size
yield items[start:end]
tasks = ["task-1", "task-2", "task-3", "task-4", "task-5"]
for batch in split_batches(tasks, 2):
print(batch)输出:
['task-1', 'task-2']
['task-3', 'task-4']
['task-5']这里没有一次性构造“所有批次组成的列表”。每轮循环需要一批数据时,生成器才通过 yield 返回一批。
这种思路常用于:
- 分页读取接口数据;
- 分批写入数据库;
- 逐行处理大型文件;
- 持续接收日志或消息;
- 构建数据处理流水线。
11. 使用 yield from 委托迭代
如果一个生成器需要依次产出多个可迭代对象中的元素,可以使用 yield from:
def all_tags():
python_tags = ["python", "iterator"]
web_tags = ["html", "css"]
yield from python_tags
yield from web_tags
print(list(all_tags()))输出:
['python', 'iterator', 'html', 'css']它相当于:
def all_tags():
python_tags = ["python", "iterator"]
web_tags = ["html", "css"]
for tag in python_tags:
yield tag
for tag in web_tags:
yield tag在简单场景中,可以把 yield from 理解为“把另一个可迭代对象中的值逐个转交出去”。
12. 常见误区
12.1 生成器不是元组
下面创建的是生成器表达式:
values = (number * 10 for number in range(3))如果想得到元组,需要显式转换:
values = tuple(number * 10 for number in range(3))
print(values)输出:
(0, 10, 20)12.2 生成器通常只能完整消费一次
names = (name.upper() for name in ["Ada", "Linus", "Guido"])
print(list(names))
print(list(names))输出:
['ADA', 'LINUS', 'GUIDO']
[]如果需要反复遍历,可以保存原始数据,并在每次使用前重新创建生成器。
12.3 创建生成器不会立即执行函数体
def demo():
print("函数开始执行")
yield 1
generator = demo()
print("生成器已经创建")
print(next(generator))输出:
生成器已经创建
函数开始执行
1直到第一次调用 next(),生成器函数才真正开始执行。
12.4 不要无条件创建无限生成器
生成器可以无限地产生数据:
def sequence():
number = 1
while True:
yield number
number += 1这种写法本身没有问题,但使用方必须知道何时停止,例如配合条件判断或 itertools.islice(),否则循环永远不会结束。
13. 应该选择列表还是生成器
适合使用列表的情况:
- 数据量较小;
- 结果需要反复使用;
- 需要通过下标随机访问;
- 需要调用
len()或直接查看全部内容。
适合使用生成器的情况:
- 数据量很大,甚至没有明确上限;
- 只需要顺序处理一次;
- 希望尽快得到第一个结果;
- 数据来自文件、网络或持续的数据流;
- 希望把多个处理步骤连接成流水线。
生成器并不是列表的替代品。选择哪一种,取决于数据规模、访问方式和代码可读性。
14. 总结
可以用下面几句话记住本文的核心:
可迭代对象:能够被 for 遍历
迭代器:记住遍历位置,通过 next() 逐个返回值
StopIteration:表示迭代已经结束
生成器:一种按需产生数据的特殊迭代器
yield:返回一个值并暂停函数,下一次从原位置继续for 循环替我们隐藏了迭代协议的细节,而生成器帮助我们用更少的代码实现迭代器。对于大型数据和一次性顺序处理任务,生成器能够在保持代码简洁的同时显著减少内存占用。