14. 再谈函数的高级用法
14.2 生成器

基本概念

在上一节中,我们讲解了迭代的概念,同时了解了迭代器的使用。由此,我们可以引出 Python 中一个特别的用法:生成器。

顾名思义,生成器,就是可以不断动态生成数据的方法。

生成器的基础,就是我们在上节中讲解的迭代器。

还记得迭代器的“延迟求值”特性吗?我们不需要一次性加载所有数据到内存里,也能遍历潜在无限的数据序列。而生成器就应用了这一特性,从而可以在迭代过程中不断生成值。

生成器的工作原理

在使用生成器之前,我们先来了解以下生成器的工作原理。

  1. 生成器函数:生成器是使用一种特殊类型的函数——生成器函数创建的。这些函数使用yield关键字而不是return。当调用生成器函数时,它不会立即执行函数体。相反,它会返回一个生成器对象。
  2. 延迟计算:生成器的关键特性是延迟计算。当我们迭代生成器时,它逐步执行生成器函数,并在遇到yield语句时暂停执行。也就是说,生成器不会立即计算或存储所有的值;相反,它会根据外部请求来即时生成它们。
  3. 状态保留:生成器会在迭代之间记住它们的状态。当我们继续迭代生成器时,它会从上次停止的地方继续,而不是从头开始。

下面让我们来看一个生成器的例子。

def my_range(n):
    i = 1
    while i <= n:
        yield i
        i += 1
 
x = my_range(10)
print(x)  # 输出:<generator object my_range at 0x10bf717b8>

在此例中,我们创建了一个生成器,生成从 1 到 10 中的所有数字。然后,我们可以使用 for 循环或 next() 函数来迭代生成器:

# 使用 for 循环迭代生成器
for i in my_range(10):
    print(i)  # 输出:1 2 3 4 5 6 7 8 9 10
 
# 使用 next() 函数迭代生成器
print(next(x))  # 输出:1
print(next(x))  # 输出:2
print(next(x))  # 输出:3

看到了吗?在我们运行上述代码时,生成器并不会一次性计算出所有的数值;相反,它只会在每次迭代时,才会计算。

这样做有两个好处:

  1. 生成器的内存利用率非常高,因为它动态生成值,不会将整个序列存储在内存中。这特别适用于处理大型或潜在无限的数据序列;
  2. 生成器可以提高代码的性能,因为它避免了预先计算和存储值的开销。这对于处理日志文件、读取大型数据集或生成数字序列等任务非常有帮助。

好了,下面就让我们来详细探究一下生成器在 Python 中的应用吧。

生成器在 Python 中的应用

在 Python 中,创建一个生成器与创建一个普通函数很类似,最关键的区别在于:我们必须使用yield关键字返回数值,而不是使用return关键字。

yield 关键字在函数内部,就表明该函数是一个生成器,这样,该函数每次就只产生一个值,同时保留其在调用之间的状态。

让我们再回顾一下上面的例子。

def my_range(n):
    i = 1
    while i <= n:
        yield i
        i += 1
 
x = my_range(10)
 
# 使用 for 循环迭代生成器
for i in x:
    print(i)  # 输出:1 2 3 4 5 6 7 8 9 10

我们在这个例子中创建了一个生成器my_range(),可以生成从 1 到 10 中的所有数字。然后,我们使用 for 循环来迭代该生成器。在迭代生成器my_range()时,它一次只会产生一个值,同时保留函数在调用之间的状态。

所以,当我们在一个函数内部使用 yield 关键字时,该函数变成一个生成器函数。生成器函数在调用时不会执行整个函数体;相反,它会返回一个生成器对象,用于迭代生成器函数生成的值。

当在生成器函数在执行中遇到 yield 语句时,函数的执行会被临时暂停;而在此时,yield 语句生成的值会被返回给调用代码。函数的局部状态被保留,包括局部变量的值。

当迭代生成器时(不管是在for循环中,还是通过调用 next()方法),函数会从其上次暂停的位置恢复执行,即在 yield 语句之后的地方继续执行,直到遇到下一个 yield 语句或者达到函数的结尾。如果到达函数的结尾,生成器会触发一个 StopIteration 异常,表示没有更多的值可生成。

让我们再看一个例子:使用生成器来创建斐波那契数列:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
 
x = fibonacci()
print(next(x))  # 输出:0
print(next(x))  # 输出:1
print(next(x))  # 输出:1
print(next(x))  # 输出:2
print(next(x))  # 输出:3
print(next(x))  # 输出:5

在此例中,fibonacci生成器不会一次性创建出所有的斐波那契数然后存储在内存中,而只是遵循“延迟求值”的方式,动态生成斐波那契数。

除了使用yield语句创建生成器函数外,我们还可以列表推导的方式生成生成器表达式。

生成器表达式是使用括号括起来的列表推导式,它会立即生成生成器。举个例子,我们可以使用以下代码创建一个生成器,生成 1 到 10 中的所有数字的平方:

x = (x**2 for x in range(1, 11))
 
print(x)  # 输出:<generator object <genexpr> at 0x10bf717b8>

在这个例子中,我们采用列表推导式的语法创建了一个生成器,用以生成 1 到 10 中的所有数字的平方。

虽然语法有所不同,但在使用上,生成器表达式与生成器函数没有区别。

关键字yieldreturn的区别

最后,我们再来比较以下关键字yield和关键字return

如前所述,在 Python 中,关键字yieldreturn都可以用来结束函数的执行并返回一个值;但是,它们却有一些明显的区别。

  1. 返回值的类型不同:return语句返回一个普通的值,而yield语句返回一个生成器。
  2. 执行方式不同:return语句直接结束函数的执行并返回一个值,而yield语句生成一个值并保留函数的状态,下次调用函数时从上次停止的地方继续执行。
  3. 应用场景不同:return语句适用于大多数情况,而yield语句只适用于创建生成器的场景。

总结

综上所述,生成器是 Python 中一项强大的工具,特别适用于处理大型数据集或需要逐个生成值的情况。

生成器通过使用yield关键字在函数内部来定义生成器函数,使函数能够在每次迭代时产生一个值并保留其状态,从而让生成器能够高效地处理大量数据,因为它们不会一次性加载全部数据到内存中,而是按需生成。

当我们在处理日志文件、读取大型数据集、或生成无限序列等任务时,往往都会用到生成器。