生成器是Python中一种特殊的迭代器,它不需要像列表那样提前把所有元素存储在内存中,而是在迭代过程中按需生成数据,非常适合处理大规模数据或者无限序列的场景。yield关键字是定义生成器的核心,理解它的工作机制就能轻松掌握生成器的使用方式。

生成器的基本概念
生成器本质上是一种迭代器,它遵循迭代器协议,也就是实现了__iter__和__next__方法,但是不需要我们手动去实现这两个方法,Python解释器会自动处理。生成器的定义方式有两种,一种是通过生成器表达式,另一种是使用yield关键字定义生成器函数。
普通函数执行时会从第一行开始运行,直到遇到return语句或者函数结束才返回结果,然后函数的所有局部状态都会被销毁。而生成器函数在调用时不会立即执行函数体,而是返回一个生成器对象,只有当对这个对象调用next()方法或者进行迭代时,函数体才会开始执行。
yield关键字的工作原理
yield关键字的作用和return类似,都可以让函数返回结果,但是二者有本质区别:return会结束函数的执行,而yield只会暂停函数的执行,并且保存当前的局部状态,下次再调用next()时,会从yield暂停的位置继续执行。
我们可以通过一个简单的例子来理解这个过程:
def simple_generator():
print("第一次执行,即将返回1")
yield 1
print("第二次执行,即将返回2")
yield 2
print("第三次执行,即将返回3")
yield 3
# 创建生成器对象
gen = simple_generator()
# 第一次调用next,执行到第一个yield暂停
print(next(gen)) # 输出:第一次执行,即将返回1 换行后输出1
# 第二次调用next,从上次yield的位置继续执行
print(next(gen)) # 输出:第二次执行,即将返回2 换行后输出2
# 第三次调用next,从上次yield的位置继续执行
print(next(gen)) # 输出:第三次执行,即将返回3 换行后输出3
# 再次调用next,函数已经执行完毕,会抛出StopIteration异常
# next(gen)
从上面的例子可以看到,每次调用next(gen)时,生成器函数就会从上次暂停的位置继续执行,直到遇到下一个yield返回结果,再次暂停。当函数执行完所有代码没有遇到yield时,再调用next()就会抛出StopIteration异常,这也是迭代器结束的标志。
生成器的常见使用场景
1. 处理大规模数据
当我们需要处理一个非常大的数据集,比如读取一个几十GB的日志文件,如果一次性把文件内容全部读入内存,很容易导致内存溢出。这时候使用生成器逐行读取就可以避免这个问题:
def read_large_file(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
# 逐行读取,每次只返回一行内容,不会占用大量内存
for line in f:
yield line.strip()
# 使用生成器读取文件
file_gen = read_large_file('large_log.txt')
# 迭代处理每一行,这里只打印前5行作为示例
count = 0
for line in file_gen:
print(line)
count += 1
if count >= 5:
break
2. 生成无限序列
生成器还可以用来生成无限序列,比如生成从0开始的自然数序列,普通列表是无法存储无限个元素的,但是生成器可以做到:
def natural_numbers():
num = 0
while True:
yield num
num += 1
# 创建自然数生成器
num_gen = natural_numbers()
# 获取前10个自然数
for i in range(10):
print(next(num_gen))
3. 实现协程的基础
yield关键字还可以配合send()方法实现简单的协程,允许我们在生成器暂停的时候向生成器内部传递数据:
def coroutine_example():
print("协程启动")
while True:
# 接收外部传递进来的值
value = yield
print(f"接收到的值:{value}")
# 创建协程对象
cor = coroutine_example()
# 启动协程,需要先调用next()或者send(None)
next(cor)
# 向协程内部传递数据
cor.send("第一个数据")
cor.send("第二个数据")
# 关闭协程
cor.close()
生成器和普通函数的区别
我们可以通过下面的表格清晰对比生成器函数和普通函数的差异:
| 对比项 | 普通函数 | 生成器函数 |
|---|---|---|
| 定义关键字 | 无特殊关键字 | 包含yield关键字 |
| 调用返回值 | 返回具体的结果值 | 返回生成器对象 |
| 执行逻辑 | 执行到return结束,状态销毁 | 执行到yield暂停,保存状态,下次继续 |
| 内存占用 | 如果返回大列表会占用大量内存 | 按需生成数据,内存占用极低 |
| 适用场景 | 处理小规模数据,一次性返回结果 | 处理大规模数据、无限序列、流式处理 |
生成器表达式的使用
除了使用yield定义生成器函数,Python还支持生成器表达式,语法和列表推导式类似,只是把方括号换成圆括号。生成器表达式会返回一个生成器对象,而不是一个列表:
# 列表推导式,会立即生成所有元素,占用内存
list_data = [x * 2 for x in range(10)]
print(type(list_data)) # 输出:<class 'list'>
print(list_data) # 输出:[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 生成器表达式,返回生成器对象,按需生成元素
gen_data = (x * 2 for x in range(10))
print(type(gen_data)) # 输出:<class 'generator'>
# 迭代生成器获取数据
for num in gen_data:
print(num)
生成器表达式适合简单的生成逻辑,如果生成逻辑比较复杂,还是建议使用生成器函数来定义,可读性会更好。
注意事项
- 生成器对象只能迭代一次,迭代完之后就会耗尽,再次迭代不会返回任何数据,如果需要重复迭代,需要重新创建生成器对象。
- 不要在生成器函数内部使用
return返回具体的值,Python中生成器函数的return只能用来结束生成器,并且return返回的值会作为StopIteration异常的说明,不会被当作生成器的返回结果。 - 使用
send()方法向生成器传递数据时,第一次调用必须先调用next()或者send(None)来启动生成器,否则会抛出TypeError异常。
掌握生成器和yield关键字的使用,能够帮助我们写出更高效的Python代码,尤其是在处理大数据场景时,能大幅降低内存占用,提升程序的运行效率。