1 生成器定义
在Python中,一边循环一边计算的机制,称之为生成器(generator)。
生成器是一个迭代器。
含有yield语句的函数是生成器函数,该函数被调用时返回一个生成器对象(yield译为产生或生成)。
生成器拥有next方法并且行为与迭代器完全相同,这意味着生成器也可以用于Python的for循环中。
另外,对于生成器的特殊语法支持使得编写一个生成器比自定义一个常规的迭代器要简单不少,所以生成器也是最常用到的特性之一。
简而言之:
yield用于def函数中,目的是将此函数作为生成器函数使用
yield用来生成数据,供迭代器的next(Iterator) 函数使用
2 生成器分类
生成器有两种,一种生成器函数,一种是生成器表达式(列表推导式)
3 使用生成器函数定义生成器
>>> def gen():
... print("第一次输出")
... yield 1
... print("第二次输出")
... yield 2
... print("第三次输出")
... yield 3
...
>>> gen()
<generator object gen at 0x7f23f7f9a780>
定义了一个函数类型gen(),但该函数内使用了关键字yield,使得这个函数成为了生成器函数。
1) 调用生成器函数将返回一个生成器
>>> gen1 = gen()
>>> gen1
<generator object gen at 0x7f23f7f9a830>
2) 第一次调用生成器的next()方法时,生成器才开始执行生成器函数(而不是构建生成器时),直到遇到yield时暂停执行(挂起),并且yield的参数将作为此次next方法的返回值;
>>> g1 = gen()
>>> next(gen())
第一次输出
1
>>> next(gen())
第一次输出
1
>>> next(g1)
第一次输出
1
>>> next(g1)
第二次输出
2
>>> g2 = g1
>>> next(g2)
第三次输出
3
注意:
(1)gen()是一个生成器函数,每一次调用,它都会重新执行,同时该函数运行到yeild()时会暂停执行,所以两次执行(函数调用)均输出相同的值。
(2)而g1 、g2是调用函数gen()返回的生成器,为了说明问题,举例如下:
说明g1 和 g2 绑定的一个生成器对象,所以在执行next()时会顺次执行。
3)之后每次调用生成器的next方法,生成器将从上次暂停执行的位置恢复执行生成器对象,直到再次遇到yield时暂停,并且同样的,yield的参数将作为next方法的返回值;
4)如果当调用next方法时生成器函数结束(遇到空的return语句或是到达函数体末尾),则这次next方法的调用将抛出StopIteration异常(即for循环的终止条件);
5)生成器函数在每次暂停执行时,函数体内的所有变量都将被封存(freeze)在生成器中,并将在恢复执行时还原,并且类似于闭包,即使是同一个生成器函数返回的生成器,封存的变量也是互相独立的。
我们的小例子中并没有用到变量,所以这里另外定义一个生成器来展示这个特点:
>>> def fibonacci():
... a = b = 1
... yield a
... yield b
... while True:
... a, b = b, a+b
... yield b
...
>>> for num in fibonacci():
... if num > 100:
... break
... print(num)
...
1
1
2
3
5
8
13
21
34
55
89
或者
>>> def fibonacci():
... a = b = 1
... yield a
... yield b
... while True:
... a, b = b, a+b
... yield b
...
>>> next(fibonacci())
1
>>> next(fibonacci())
1
>>> next(fibonacci())
1
>>> f = fibonacci()
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
8
>>> next(f)
13
>>> next(f)
21
>>> next(f)
34
>>> next(f)
55
>>> next(f)
89
>>> next(f)
144
>>> next(f)
233
>>> next(f)
377
>>> next(f)
610
>>> next(f)
987
看到while True可别太吃惊,因为生成器可以挂起,所以是延迟计算的,无限循环并没有关系。这个例子中我们定义了一个生成器用于获取斐波那契数列。
4 一种是生成器表达式
生成器表达式(采用的是小括号“()”形式)类似于列表推导式(采用的是小括号“[ ]”形式),生成器的返回值是按每次next()调用产生一个对象,需要的存储空间较小,而列表推导式则是一次性构建一个列表库,需要的存储空间较大。
#列表表达式,生成一个元素库
>>> a = [x for x in range(1,10)]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9]
#生成器
>>> b = (x for x in range(1,10))
>>> b
<generator object <genexpr> at 0x7fb2bf6f24c0>
生成器表达式与列表推导式区别
>>> sum([i for i in range(100000000)])
>>>4999999950000000
>>> sum((i for i in range(100000000)))
>>>4999999950000000
虽然结果都能算出来两者之间,但是列表推导式耗时长、易卡顿,而生成器表达式计算速度快、不卡顿。
详细可参考 如何更好地理解Python迭代器和生成器?知乎
5 生成器函数的FAQ
1) 生成器函数可以带参数(带形参)吗?
生成器函数也是函数的一种,所以可以带参数
>>> def counter(start=0):
... while True:
... yield start
... start += 1
...
这是一个从指定数开始的计数器。
2)生成器函数中可以有return 语句吗?
不可以,因为生成器函数已有默认了返回值——生成器,所以不能重新用return返回值(即使return None也不行),如果使用return返回值则会抛出语法错误异常。
>>> def gen():
... print("输出1")
... yield 1
... print("输出2")
... return "return语句"
... yield 2
... print("输出2")
...
>>> g = gen()
>>> next(g)
输出1
1
>>> next(g)
输出2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: return语句
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
补充
1 沿着某条路线依次访问,且只访问一次,该种现象成为遍历
2 迭代器的访问是一次性的,指针向下移动时,不会后退;也即一次性调用。
3 生成器也是迭代器
def gen_ite():
yield 1
it = gen_ite()
a = iter(it)
print(id(it))
print(id(a))
4 列表与生成器之间的关系
#生成一个列表库
>>> c = list(range(10))
>>> c
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#将一个新变量进行绑定一个新生成的序列
>>> c1 = [x for x in c]
>>> c1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#将一个新变量绑定生成器
>>> c2 = (x for x in c)
>>> c2
<generator object <genexpr> at 0x7fb2bf6f2518>
#生成器进行序列化
>>> list(c2)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#改变原变量c中键值为1的值
>>> c[1] = 100
#c1为新序列绑定的变量,改变原变量值对其没有影响
>>> c1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#c2其实是生成器中的第二种(列表推导式方式)产生生成器的方式(第一种是yield语句)。
#同时生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表
#这里c2为空列表的主要原因是指针后移造成的
>>> c2
<generator object <genexpr> at 0x7fb2bf6f2518>
>>> list(c2)
[]
下段代码阐述生成器函数是按需产生结果
>>> c = list(range(10))
>>> c2 = (x for x in c)
>>> c
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> c[1] = 100
>>> c
[0, 100, 2, 3, 4, 5, 6, 7, 8, 9]
>>> next(c2)
0
>>> c
[0, 100, 2, 100, 4, 5, 6, 7, 8, 9]
#生成器随着原数据库变化而变化
>>> next(c2)
100