跳转至

Python 进阶教程系列 3:生成器

本文是 Python 进阶教程系列 3,主要介绍了生成器 generatoryield 的机制和 nextsend 的用法,并展示了如何用 generator 生成无限序列。

在循环中使用生成器作为迭代对象,就不用将所有需要遍历的值都全部计算出来再迭代,而是可以每迭代一次就计算一个需要遍历的值。

这种写法有以下几个好处:

  1. 节省内存资源:生成器以延迟计算的方式逐个生成数据,不会一次性生成所有数据,因此可以避免在内存中存储大量数据,从而减少内存资源的占用。
  2. 更快的计算速度:由于生成器以延迟计算的方式逐步生成数据,避免了一次性计算所有数据所带来的资源浪费,因此生成器往往比一次性生成所有数据的方式更加高效。
  3. 更灵活的逻辑处理:生成器可以动态地生成数据,并且可以支持迭代过程中的数据清洗、处理等操作,从而让代码更加灵活、简单。

yield 构造一个生成器

下面是一个简单的 Python 示例,展示了如何使用 yield 构造一个生成器:

Python
def even_numbers(n):
    i = 0
    while i <= n:
        if i % 2 == 0:
            yield i
        i += 1

# 调用生成器
for num in even_numbers(10):
    print(num)

在上面的代码示例中,我们定义了一个名为 even_numbers() 的函数,它的参数是 n,表示我们要生成的偶数的下界。在函数体内部,我们使用 while 循环和 if 语句,判断每个数字是否为偶数,如果是,就使用 yield 语句将它返回。

在函数调用时,我们使用 for 循环遍历生成器,依次打印出每个偶数。注意,由于生成器只会在需要时才生成下一个元素,因此它非常适合处理大量数据或者需要动态产生数据的场景。

next()

让我们更详细地来解释一下生成器是如何工作的。

在 Python 中,生成器是一种特殊的函数,其定义方式与普通函数类似,但它使用了 yield 语句,而不是 return 语句。yield 语句可以用来暂停函数的执行,并将一个值返回给调用方。但是,与使用 return 语句不同的是,函数的状态并没有被销毁。相反,函数的状态被保存在内存中,以便在下一次调用 yield 时恢复它的执行。

当我们调用一个生成器函数时,Python 解释器会创建一个生成器对象,并返回它。这个对象可以被看作是一个可迭代对象,我们可以使用 for 循环来遍历它,或者使用 next() 函数调用它的 next() 方法,逐个获取生成器生成的元素。

每次调用 next() 方法时,生成器都会从上次暂停的位置继续执行,并在遇到下一个 yield 语句时再次暂停,将生成的值返回给调用方。这个过程会一直持续,直到函数中没有更多的 yield 语句可供执行。此时,生成器便会自动抛出一个 StopIteration 异常,表示生成器已经到达了末尾。

因此,正是由于 Python 解释器能够保存函数的上下文状态,才使得生成器可以在需要时、按需产生数据。这种“流式计算”的方式可以大大降低内存消耗,特别适合处理海量数据或者无法一次性获取的数据源。

使用 next() 方法的代码示例

当我们需要一次只获取一个元素时,就可以使用 next() 方法来从生成器中获取值,next() 方法会一次执行一个生成器,直到遇到下一个 yield 语句,然后把生成的值返回。

下面是一个示例代码,展示如何使用 next() 方法来获取生成器的值:

Python
def counter(maximum):
    i = 0
    while i < maximum:
        val = yield i
        if val is not None:
            i = val
        else:
            i += 1

g = counter(10)
print(next(g))  # 输出 0
print(next(g))  # 输出 1
print(next(g))  # 输出 2
print(g.send(5))  # 将 i 设置为 5,输出 5
print(next(g))  # 输出 6

image-20230306094718152

在上面的代码中,我们创建了一个名为 counter 的生成器函数,它接受一个整数参数 maximum,表示生成器最大的值。在生成器内部,我们定义变量 i 并使用 while 循环不断生成值。每个生成的值都会使用 yield 语句返回,并在下一次调用的时候从上一个 yield 语句处继续执行。

接下来,我们创建了一个生成器对象 g,并演示了如何使用 next() 方法来获取它的值。首先,我们使用 print(next(g)) 获取生成器的第一个值,输出 0;然后我们再次调用 next() 方法,获取生成器的第二个值,输出 1;依次类推,直到输出 2

接着,我们使用生成器的 send() 方法将生成器的内部状态修改为 5,然后再次调用 next() 方法来获取下一个值。此时,生成器的最大值被设置为 5,然后我们输出了 5。

然后,我们再次使用 next() 方法来获取下一个生成器值,输出 6

send()

在刚才提供的示例代码中,send() 方法可以用来往生成器内部发送一个值。它的作用在于,让生成器从上一个 yield 语句处继续执行,并将一个新的值传递给生成器。这个新的值可以在生成器内部进行处理,然后在下一次调用 next() 方法时返回。

在示例代码中,我们给生成器对象 g 执行了一次 send(5) 方法,将生成器的内部状态修改为了 i = 5。这使得下一次调用 next() 方法时,生成器会直接从 i = 5 开始运行,而不是从 i = 3 或者 i = 2 开始运行。

同时,我们还使用 if val is not None: 判断了 send() 方法发送的值是否为空,如果不为空,则将其覆盖 i,否则就按照正常逻辑进行迭代。这样,就可以在生成器内部动态修改状态,实现灵活的数据生成和流处理。

需要注意的是,如果你想使用 send() 方法修改生成器的内部状态,那么你必须在调用 send() 方法之前执行一次 next() 方法,以启动生成器的执行过程。否则,生成器无法从其上一个 yield 语句处开始执行,也就无法正常响应 send() 方法发送的值。

生成无限序列

生成器的一个重要特点是它们可以生成无限序列。通常,生成无限序列将需要大量的内存和时间来存储和计算。但是,使用生成器,我们可以逐步生成序列中的每个元素,而无需一次性生成整个序列。

以下是一个生成器函数的示例,它生成一个无限的偶数序列:

Python
def even_numbers():
    n = 0
    while True:
        yield n
        n += 2

此函数从n=0开始,并通过在每次迭代中将 2 添加到n来生成一个无限的偶数序列。yield关键字返回n的当前值,并暂停函数,直到请求下一个值。

你可以使用生成器来生成任何类型数据的无限序列,包括数字、字符串和复杂对象。以下是一个生成器的示例,它生成一个无限的字符串序列:

Python
def string_generator():
    s = 'a'
    while True:
        yield s
        s += 'a'

此函数从s='a'开始,并通过在每次迭代中将另一个'a'添加到s来生成一个无限的字符串序列。

当处理大型数据集或需要即时生成值序列时,生成器非常有用。它们占用内存少,可以用于生成太大而无法适应内存的序列。

总之,Python 生成器是生成无限数据序列的强大工具。它们允许你生成一个值的序列,而不必在内存中创建整个列表,因此在处理大型数据集或即时生成序列时是一个很好的选择。

评论