Skip to main content

03-生成器

生成器(Generator)是 Python 中一种非常有用的工具,用于简化迭代器的创建,并提高内存使用效率。下面将从多个角度介绍生成器的概念和使用方法。

1. 什么是生成器?

生成器是一种特殊的 迭代器,它允许你在函数执行过程中 “暂停”,并在下一次调用时继续执行。与传统的函数不同,生成器使用 yield 关键字返回数据,每次调用 next() 时从上次 yield 的地方恢复执行。这种 惰性求值 机制使得生成器在处理大量数据时非常高效,因为它不需要一次性将所有数据加载到内存中。

迭代器用于从数据集中取出元素,而生成器用于 “凭空” 生成元素,它不会一次性将所有元素全部生成,而是按需一个一个地生成,所以从头到尾都只需占用一个元素地空间。

2. 如何定义生成器?

有两种常见的方式可以定义生成器:

2.1 使用生成器函数

生成器函数类似于普通函数,但它使用 yield 关键字返回数据。每次执行到 yield 时,函数会暂停,并保存当前状态,等待下一次调用。

示例:

def simple_generator():
"""这是一个生成器函数,调用后返回的是一个生成器对象"""
print("生成器开始执行")
yield 1
print("生成器继续执行")
yield 2
print("生成器结束")
yield 3
return "没有值了"

# 调用生成器函数,返回生成器对象,当调用 simple_generator() 时,它不会立即执行,而是返回一个生成器对象,等你调用 next() 的时候,它才开始执行。
gen = simple_generator()
# 生成器对象使用 next 来获取值
print(next(gen)) # 输出:生成器开始执行 \n 1
print(next(gen)) # 输出:生成器继续执行 \n 2
print(next(gen)) # 输出:生成器结束 \n 3
print(next(gen)) # 抛出 StopIteration 异常,异常值为 "没有值了"

在这个例子中,simple_generator 每次遇到 yield 返回一个值,并暂停执行,等待下一次调用 next()

为什么 return 的值会变成 StopIteration 的值?

这是 Python的生成器协议(Generator Protocol) 决定的。

  • 当生成器函数执行到 yield 时,会暂停并返回值。

  • 当所有的 yield 执行完,且生成器遇到 return 或函数正常结束,就意味着没有更多的值可以生成了。

  • 此时,生成器会抛出 StopIteration 异常,且这个异常的 value 属性,就是你在 return 后写的内容。

验证一下

你可以显式地捕获这个异常看看:

gen = simple_generator()
try:
print(next(gen)) # 输出:生成器开始执行 \n 1
print(next(gen)) # 输出:生成器继续执行 \n 2
print(next(gen)) # 输出:生成器结束 \n 3
print(next(gen)) # 会触发 StopIteration
except StopIteration as e:
print("捕获到了 StopIteration,返回值是: ", e.value)

输出:捕获到了 StopIteration,返回值是: 没有值了

取值方式二:

生成器对象也是一个可迭代对象,也可以使用迭代方式遍历;

for i in simple_generator():
print(i)

# 或者 迭代 生成器对象
for i in gen:
print(i)

2.2 使用生成器表达式

生成器表达式的语法与列表推导式类似,但它返回的是生成器对象而不是整个列表,从而避免一次性创建所有数据。

示例:

# 生成器表达式
gen_exp = (x * x for x in range(5))

# 遍历生成器表达式
for num in gen_exp:
print(num)

该表达式会逐步计算 x * x,而不是将所有值都保存到列表中。

ps:需要补充下 列表推导式 的相关内容。

列表推导式提供了一种简洁的方式来生成新的列表。其基本语法结构如下:

[表达式 for 变量 in 可迭代对象 if 条件]
  • 表达式:生成列表中元素的值,通常依赖于后面的变量。
  • 变量:用于依次取出可迭代对象中的每个元素。
  • 可迭代对象:如列表、元组、字符串、range() 生成器等。
  • 条件(可选):用于过滤,只对满足条件的元素执行表达式。

生成器表达式和列表推导式的写法区别就是:列表推导式最外层是一个 [],而生成器表达式最外层是一个 (),为什么是一个 () 呢?因为生成器是一个对象,对象的实例化都是使用 () 的。

示例:

1.基本示例

生成从 0 到 9 的列表:

numbers = [x for x in range(10)]
print(numbers) # 输出: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

2.带条件的示例

生成从 0 到 9 中所有偶数的平方:

squares = [x * x for x in range(10) if x % 2 == 0]
print(squares) # 输出: [0, 4, 16, 36, 64]

3.嵌套列表推导式

将二位列表(矩阵)中的所有数字提取到一个一维列表中:

matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
flat = [num for row in matrix for num in row]
print(flat) # 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]

通过这种方式,列表推导式可以使代码更简洁、更直观,同时也提高了代码的可读性。

3. 生成器的优点

  • 内存友好:生成器按需生成数据,不会一次性将所有数据加载到内存中,非常适合处理大数据量。
  • 简洁性:使用生成器可以让代码更简洁,尤其是在需要编写复杂的迭代逻辑时。
  • 无限序列:生成器可以创建无限序列,比如斐波那契数列,通过 yield 可以无限输出下一个值而不会耗尽内存。

4. 使用生成器的注意事项

  • 状态保存:生成器在暂停时会保存局部变量的状态,这对于某些特定的逻辑是非常有用的,但也需要注意变量状态的影响。
  • 一次性迭代:生成器一旦迭代完成,就不能再重新迭代。如果需要多次使用生成器生成的数据,可以考虑转换成列表(如果数据量允许的话)或者重新生成生成器。

5. 综合示例

下面是一个生成器函数的综合示例,该函数用于生成斐波那契数列的前n个数:

def fibonacci(n):
a, b = 0, 1
count = 0
while count < n:
yield a
a, b = b, a + b # 同时赋值语句,等价于 a = b; b = a + b
count += 1


# 使用生成器生成斐波那契数列
for num in fibonacci(10):
print(num)

在这个例子中,fibonacci 函数使用 yield 返回每个斐波那契数。通过 for 循环,我们可以逐个获得这些数值,而不需要一次性生成整个序列。

总结来说,生成器是 Python 中实现迭代器的一个简洁而强大的工具,通过 yield 关键字可以让函数逐步返回值,节省内存,并使代码逻辑更加清晰。在实际开发中,它们常用于大数据流处理、文件读取和无限序列的生成等场景。