Skip to content

yield 生成器

生成器(英文:generator)器是一类特殊的迭代器。

生成器推导式

python
>>> my_generator = (x * x for x in range(4))

这是不是跟列表推导式很类似呢?仔细观察,它不是列表,如果这样的得到的才是列表:

python
>>> my_list = [x * x for x in range(4)]

以上两的区别在于是 [] 还是 (),虽然是细小的差别,但是结果完全不一样。

创建 my_listmy_generator 的区别仅在于最外层的 [ ]( ) , my_list 是一个列表,而 my_generator 是一个生成器。我们可以直接打印出列表 L 的每一个元素,而对于生成器 my_generator,我们可以按照迭代器的使用方法来使用,即可以通过 next() 函数、for循 环、list() 等方法使用。

python
>>> 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 中,它作为一个关键词(你在变量、函数、类的名称中就不能用这个了),是生成器的标志。

python
>>> def gen():
...     yield 0
...     yield 1
...     yield 2
...
>>> gen
<function gen at 0x000001C6AF363E20>

建立了一个非常简单的函数,跟以往看到的函数唯一不同的地方是用了三个 yield 语句。然后进行下面的操作:

python
>>> g = gen()
>>> g
<generator object gen at 0x000001C6AF7A05F0>

上面建立的函数返回值是一个生成器(generator)类型的对象。

python
>>> 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(),说明它是迭代器。既然如此,当然可以:

python
>>> 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 语句的作用,就是在调用的时候返回相应的值。详细剖析一下上面的运行过程:

  1. g = gen():除了返回生成器之外,什么也没有操作,任何值也没有被返回。
  2. next(g):直到这时候,生成器才开始执行,遇到了第一个 yield 语句,将值返回,并暂停执行(有的称之为挂起)。
  3. next(g):从上次暂停的位置开始,继续向下执行,遇到 yield 语句,将值返回,又暂停。
  4. next(g):重复上面的操作。
  5. next(g):从上面的挂起位置开始,但是后面没有可执行的了,于是 next() 发出异常。

从上面的执行过程中,发现 yield 除了作为生成器的标志之外,还有一个功能就是返回值。那么它跟 return 这个返回值有什么区别呢?

yield 与 return

为了弄清楚 yieldreturn 的区别,我们写两个没有什么用途的函数:

python
>>> 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

python
>>> 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 语句,它就是生成器,也是迭代器。这种方式显然比前面写迭代器的类要简便多了。但,并不意味着上节的就被抛弃。是生成器还是迭代器,都是根据具体的使用情景而定。

生成器接受参数

python
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.

生成器只会记录规则,不会记录值

案例:协程计算平均值

python
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))

总结

生成器与普通函数的区别:生成器返回的是记录与规则,普通函数返回结果