Logo
Section 1 / 7

生成器的工作原理:字母迭代器

学习生成器的工作原理,以及如何使用它来实现自己的迭代器。

导入

生成器的工作原理:字母迭代器

学习生成器的工作原理,以及如何使用它来实现自己的迭代器。

生成器

我们在迭代器章节中了解到,可以通过定义一个包含特定方法的类来自定义迭代器。这种方法虽然可行,但需要编写较多样板代码,并且迭代逻辑的编写方式较为复杂。

生成器 提供了一种简单的方式来实现多种类型的迭代器,使用简洁的语法,通常也具有更直观的程序流程。

示例:字母迭代器

在迭代器章节中,我们实现了一个迭代器,逐个返回字母表中的字母,直到 'e' 为止。以下是等价的生成器实现:

1def alphabet(): 2 for c in 'abcde': 3 yield c 4 5for x in alphabet(): 6 print(x)

alphabet 看起来像一个普通函数,但实际上它是一个生成器。我们可以从它包含 yield 语句(第3行)而非 return 语句来判断这一点。

在 for 循环中,alphabet 的用法与 range 类似。如果你使用 range(5),它会遍历 0 到 4 的数值。而 alphabet 做类似的事,只是遍历的是字符 'a' 到 'e'。

这个生成器仅用于演示。实际上,直接使用字符串 'abcde' 也能达到完全相同的效果。

生成器的工作原理

为了理解生成器如何工作,我们将 for 循环展开如下:

1def alphabet(): 2 for c in 'abcde': 3 yield c 4 5g = alphabet() 6 7x = next(g) 8print(x) 9 10x = next(g) 11print(x)

首先,我们调用 alphabet()。与普通函数不同,生成器返回一个 generator 对象并将其存储在变量 g 中。关键点在于,alphabet 函数体中包含 yield 语句,而不是 return。这正是它区别于普通函数的地方。Python 识别到这一点后,就会创建一个生成器对象。

生成器对象的行为类似于迭代器。当我们对其调用 next() 时,它会返回序列中的第一个值 'a'。再次调用 next(),则返回下一个值 'b',依此类推。

那么实际发生了什么?首先要明确的是,生成器函数体中的代码并不会在调用 alphabet() 时立即执行。正如真正的迭代器一样,生成器采用惰性求值策略——只有在真正请求值时才开始执行。

第一次调用 next() 时,Python 开始执行 alphabet 函数体。它启动 for 循环,首次进入循环时,变量 c 的值为 'a'。接着遇到 yield 语句。

此时,Python 暂停执行 alphabet 函数,并将 c 的值返回给主调用代码。但关键在于:yield 会保存函数在退出前的完整状态。

随后,主代码执行 print(x),输出值 'a'。接着再次调用 next()。这一次,Python 并不会从头开始执行 alphabet 函数,而是跳转回上一次 yield 语句之后的位置。函数的状态被恢复到 yield 执行时的精确状态。

alphabet 函数继续执行 for 循环的下一次迭代。这次它从字符串中取出第二个字符 'b',并在下一次 yield 时将其返回。

每次主循环调用 next(),这个过程都会重复。最终,alphabet 函数中的 for 循环执行完毕。此时函数不再调用 yield,而是自然结束。生成器对象在此时抛出 StopIteration 异常,通知调用方迭代已结束。

由此可见,控制权在主代码和 alphabet 函数之间来回切换。正因如此,生成器有时被称为 协程。这与普通函数(也称 子例程)形成对比:在子例程中,调用代码将全部控制权交给子例程,直到其完成;而在协程中,控制权是双向流动的。

请注意,这与多线程不同。在多线程中,两段不同的代码可能同时运行(在不同核心上或通过时间片共享单个核心)。而协程只存在单一执行线程,它以完全可预测的方式在两个不同的代码路径之间切换。

推导式

练习:斐波那契数列生成器