生成器

生成器的作用

每次只生成一个值,而不是直接创建大的元组,节省内存消耗。

第一个例子

先看一个交互式例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> def make_counter(x):
... print('entering make_counter')
... while True:
... yield x # 表明这是个生成器,所有变量、局部数据将在生成器里通过yield保存
... print('incrementing x')
... x = x + 1
...
>>> counter = make_counter(2) # 获得一个迭代器
>>> counter
<generator object at 0x001C9C10>
>>> next(counter) # 恢复一个变量,执行到生成器的下一条yield语句
entering make_counter
2
>>> next(counter)
incrementing x
3
>>> next(counter)
incrementing x

make_counter 中出现的 yield 命令的表示这不是一个普通的函数,而是一个一次生成一个值的特殊类型函数。调用该函数将返回一个可用于生成连续 x 值的 生成器【Generator】

为创建 make_counter 生成器的实例,仅需像调用其它函数那样对它进行调用。注意该调用并不完全执行函数里的指令,由上例可见它并不指定第一行的print指令。

next() 函数以一个生成器对象为参数,并返回其下一个值。对 counter 生成器第一次调用 next() ,它针对第一条 yield 语句执行 make_counter() 中的代码,然后返回所产生的值。在此情况下,该代码输出将为 2,因其仅通过调用 make_counter(2) 对生成器进行初始创建。

对同一生成器对象反复调用 next() 将确切地从上次调用的位置开始继续,直到下一条 yield 语句。所有的变量、局部数据等内容在 yield 时被保存,在 next() 时被恢复 。下一行代码等待被执行以调用 print() 打印出 incrementing x 。之后,执行语句 x = x + 1。然后它继续通过 while 再次循环,而它再次遇上的第一条语句是 yield x,该语句将保存所有一切状态,并返回当前 x 的值。

yield 暂停一个函数。next() 从其暂停处恢复其运行。

由于 make_counter 设置了一个无限循环,理论上可以永远执行该过程,它将不断递增 x 并输出数值。还是让我们看一个更加实用的生成器用法。

斐波那契生成器

1
2
3
4
5
def fib(max):
a, b = 0, 1
while a < max:
yield a
a, b = b, a + b

斐波那契序列是一系列的数字,每个数字都是其前两个数字之和。它从 0 和 1 开始,初始时上升缓慢,但越来越快。启动该序列需要两个变量:从 0 开始的 a,和从 1 开始的 b 。

a 是当前序列中的数字,因此对它进行 yield 操作。b 是序列中下一个数字,因此将它赋值给 a,但同时计算下一个值 (a + b) 并将其赋值给 b 以供稍后使用。

因此,现在有了一个连续输出斐波那契数值的函数。当然,还可以使用递归来完成该功能,但这个方式更易于阅读。同样,它也与 for 循环合作良好。

1
2
3
4
5
6
>>> from fibonacci import fib
>>> for n in fib(1000):
... print(n, end=' ')
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> list(fib(1000))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

可以在 for 循环中直接使用像 fib() 这样的生成器。for 循环将会自动调用 next() 函数,从 fib() 生成器获取数值并赋值给 for 循环索引变量。

生成器表达式

生成表达式类似生成器函数,只不过它不是函数。

示例

1
2
3
4
5
6
7
8
9
10
>>> unique_characters = {'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}
>>> gen = (ord(c) for c in unique_characters)
>>> gen
<generator object <genexpr> at 0x00BADC10>
>>> next(gen)
69
>>> next(gen)
68
>>> tuple(ord(c) for c in unique_characters)
(69, 68, 77, 79, 78, 83, 82, 89)

说明

  • 生成器表达式类似一个yield值的匿名函数。表达式本身看起来像列表解析, 但不是用方括号而是用圆括号包围起来。
  • 生成器表达式返回迭代器。
  • 调用 next(gen) 返回迭代器的下一个值。
  • 如果你愿意,你可以将生成器表达式传给tuple(), list(), 或者 set() 迭代所有的值并且返回元组,列表或者集合。在这种情况下,你不需要一对额外的括号 — 将生成器表达式 ord(c) for c in unique_characters 传给 tuple() 函数就可以了, Python 会推断出它是一个生成器表达式。

使用生成器表达式取代列表解析可以同时节省 cpu 和 内存。如果你构造一个列表的目的仅仅是传递给别的函数,(比如传递给tuple() 或者 set()), 用生成器表达式替代吧!

Comments