yield 生成器
生成器(英文:generator)器是一类特殊的迭代器。
生成器推导式
>>> my_generator = (x * x for x in range(4))
这是不是跟列表推导式很类似呢?仔细观察,它不是列表,如果这样的得到的才是列表:
>>> my_list = [x * x for x in range(4)]
以上两的区别在于是 []
还是 ()
,虽然是细小的差别,但是结果完全不一样。
创建 my_list
和 my_generator
的区别仅在于最外层的 [ ]
和 ( )
, my_list 是一个列表,而 my_generator
是一个生成器。我们可以直接打印出列表 L 的每一个元素,而对于生成器 my_generator,我们可以按照迭代器的使用方法来使用,即可以通过 next()
函数、for循 环、list()
等方法使用。
>>> my_generator = (x * x for x in range(4))
>>> next(my_generator)
0
>>> next(my_generator)
1
>>> next(my_generator)
4
>>> next(my_generator)
9
>>> next(my_generator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> my_generator = (x * x for x in range(4))
>>> for x in my_generator:
... print(x)
...
0
1
4
9
难道生成器就是把列表解析中的 []
换成 ()
就行了吗?这仅仅是生成器的一种表现形式和使用方法罢了,仿照列表解析式的命名,可以称之为“生成器解析式”(或者:生成器推导式、生成器表达式)。
生成器解析式是有很多用途的,在不少地方替代列表,是一个不错的选择。特别是针对大量值的时候。列表占内存较多,迭代器(生成器是迭代器)的优势就在于少占内存,因此无需将生成器(或者说是迭代器)实例化为一个列表,直接对其进行操作,方显示出其迭代的优势。
生成器定义
yield 这个词在汉语中有“生产、出产”之意,在 Python 中,它作为一个关键词(你在变量、函数、类的名称中就不能用这个了),是生成器的标志。
>>> def gen():
... yield 0
... yield 1
... yield 2
...
>>> gen
<function gen at 0x000001C6AF363E20>
建立了一个非常简单的函数,跟以往看到的函数唯一不同的地方是用了三个 yield 语句。然后进行下面的操作:
>>> g = gen()
>>> g
<generator object gen at 0x000001C6AF7A05F0>
上面建立的函数返回值是一个生成器(generator)类型的对象。
>>> dir(g)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
在这里看到了__iter__()
和 next()
,说明它是迭代器。既然如此,当然可以:
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
从这个简单例子中可以看出,那个含有 yield
关键词的函数返回值是一个生成器类型的对象,这个生成器对象就是迭代器。
我们把含有 yield
语句的函数称作生成器。生成器是一种用普通函数语法定义的迭代器。通过上面的例子可以看出,这个生成器(也是迭代器),在定义过程中并没有像上节迭代器那样写__inter__()
和 next()
,而是只要用了 yield
语句,那个普通函数就神奇般地成为了生成器,也就具备了迭代器的功能特性。
yield
语句的作用,就是在调用的时候返回相应的值。详细剖析一下上面的运行过程:
g = gen()
:除了返回生成器之外,什么也没有操作,任何值也没有被返回。next(g)
:直到这时候,生成器才开始执行,遇到了第一个yield
语句,将值返回,并暂停执行(有的称之为挂起)。next(g)
:从上次暂停的位置开始,继续向下执行,遇到yield
语句,将值返回,又暂停。next(g)
:重复上面的操作。next(g)
:从上面的挂起位置开始,但是后面没有可执行的了,于是next()
发出异常。
从上面的执行过程中,发现 yield
除了作为生成器的标志之外,还有一个功能就是返回值。那么它跟 return
这个返回值有什么区别呢?
yield 与 return
为了弄清楚 yield
和 return
的区别,我们写两个没有什么用途的函数:
>>> def r_return(n):
... print("进入了函数.")
... while n > 0:
... print("返回内容之前")
... return n
... n -= 1
... print("返回内容之后")
...
>>> rr = r_return(3)
进入了函数.
返回内容之前
>>> rr
3
从函数被调用的过程可以清晰看出,rr = r_return(3)
,函数体内的语句就开始执行了,遇到 return,将值返回,然后就结束函数体内的执行。所以 return
后面的语句根本没有执行。
下面将 return
改为 yield
:
>>> def y_yield(n):
... print("进入了函数.")
... while n > 0:
... print("返回内容之前")
... yield n
... n -= 1
... print("返回内容之后")
...
>>> yy = y_yield(3) # 没有执行函数体内语句
>>> next(yy) # 开始执行
进入了函数.
返回内容之前
3
>>> # 遇到 yield,返回值,并暂停
>>> next(yy) # 从上次暂停位置开始继续执行
返回内容之后
返回内容之前
2
>>> # 又遇到 yield,返回值,并暂停
>>> next(yy) # 重复上述过程
返回内容之后
返回内容之前
1
>>> next(yy) # 没有满足条件的值,抛出异常
返回内容之后
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
结合注释和前面对执行过程的分析,读者一定能理解 yield
的特点了,也深知与 return
的区别了。
一般的函数,都是止于 return
。作为生成器的函数,由于有了 yield
,则会遇到它挂起,如果还有 return
,遇到它就直接抛出 SoptIteration
异常而中止迭代。
经过上面的各种例子,已经明确,一个函数中,只要包含了 yield
语句,它就是生成器,也是迭代器。这种方式显然比前面写迭代器的类要简便多了。但,并不意味着上节的就被抛弃。是生成器还是迭代器,都是根据具体的使用情景而定。
生成器接受参数
def simple_coroutine():
print('生成器启动')
n = yield
print('生成器接收到的参数为 %s' % n)
my_coro = simple_coroutine()
next(my_coro)
my_coro.send("hello")
当执行到 my_coro.next()
的时候,生成器开始执行,在内部遇到了 yield
挂起。注意在生成器函数中,n = yield
中的 n = yield
是一个表达式,并将接收到的结果赋值给 n。
当执行 my_coro.send("hello")
的时候,原来已经被挂起的生成器(函数)又被唤醒,开始执行 n = yield
,也就是讲 send()
方法发送的值返回。这就是在运行后能够为生成器提供值的含义。
如果接下来再执行 next(my_coro)
会怎样?
什么也没有,其实就是返回了 None
。按照前面的叙述,读者可以看到,这次执行 r.next()
,由于没有传入任何值,yield
返回的就只能是 None
.
生成器只会记录规则,不会记录值
案例:协程计算平均值
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
coro_avg = averager()
next(coro_avg)
print(coro_avg.send(10))
print(coro_avg.send(30))
print(coro_avg.send(5))
总结
生成器与普通函数的区别:生成器返回的是记录与规则,普通函数返回结果