Skip to main content

生成器

这篇文章我们讲生成器。这个内容之前也是提到过很多次,但是一直都没有系统地讲。上篇文章刚讲完迭代器,我们顺便就把这个生成器讲一下。

强烈推荐大家在看今天的生成器文章之前,先把上一篇迭代器的文章看完。为什么呢?因为生成器就是一种特殊的迭代器。当你理解了什么是迭代器之后,你理解生成器就比较容易了。

生成器与迭代器的相似之处​

那生成器跟迭代器有很多相似的地方。比如说下面这一块,我们的 g 就是一个生成器。那由于生成器本身就是一个特殊的迭代器,所以我们当然可以用 for i in g 这种方式来使用这个生成器。我们看一下这个程序的结果。我们看,它打出了五、四、三、二、一。

第 1 步 · 用 for 遍历生成器1 / 5
main.py
def gen(num):
while num > 0:
yield num
num -= 1
return
g = gen(5)
for i in g:
print(i)

我们先别管这个生成器是怎么运行的。那生成器跟迭代器同样相似的地方,就是它也可以用 next 函数。我们看,我们可以通过 next 函数把它第一个值先拿出来,然后再做一个 for loop。这样它打出来的就是四、三、二、一了。

第 2 步 · 先用 next 取出第一个值2 / 5
main.py
def gen(num):
while num > 0:
yield num
num -= 1
return
g = gen(5)
first = next(g)
for i in g:
print(i)

那生成器跟迭代器相似的性质和用法,我们的这篇文章就不讲了,大家可以去看迭代器的部分。我们这篇主要讲它们不同的地方。

生成器函数与生成器对象​

那在我们聊之前,首先要区分两个概念。这两个概念经常被混用,就是生成器函数和生成器对象。我们有时候管这两个东西都叫生成器。那相比于我们之前介绍的迭代器,它就是一个正儿八经的 class,然后这个 class 给它做一个实例化,然后出来一个对象。这都是非常基础的语法,所以你不是那么容易混淆。

但是在生成器这块会稍有不同。这个 def gen 的 gen,我们叫做生成器函数。然后 g = gen(5),这个 g 我们叫做生成器对象。

那如果你是一个对 Python 半熟不熟的状态,你看到这可能会觉得很奇怪。因为这个 gen 里面有一个 return,然后你就想着这个 gen(5),不应该直接 return 一个 None 留给 g 吗?

那这个地方是 Python 在编译时期的一个小 trick。就是当它发现一个函数的定义里面有 yield 这个关键词的时候,它就不会把这个函数当成一个普通的函数来处理。Python 会给这个函数打上一个标签,然后说这是一个生成器函数。虽然它和我们平时使用的函数都一样,都是 def,然后一个函数名,然后括号,但是这两个函数在 Python 的眼里是完全不同的。

调用生成器函数,会生成一个生成器对象。有点像个绕口令。就是当你做这个第七行这个 gen(5) 的时候,它不会返回值,而会返回一个生成器对象保存到 g 里。一定要记住这一点啊。

就是这个生成器函数返回这个生成器对象,里面这个 yield、return 都不是它的返回值。只有当你对这个生成器对象使用 next 函数的时候,它才开始真正运行它的函数本体。

YIELD_VALUE 如何暂停函数​

那我们老规矩,看一下它的 bytecode。

import dis

s = """
def gen(num):
while num > 0:
yield num
num -= 1
return

g = gen(5)
first = next(g)

for i in g:
print(i)
"""
dis.dis(s)

那这个生成器函数比较有意思的 bytecode,就是这个 YIELD_VALUE。当 next 这个函数作用于这个生成器的时候,它会运行这段 code object。然后运行到 YIELD_VALUE 的时候发生了什么事呢?

case TARGET(YIELD_VALUE): {
retval = POP();

if (co->co_flags & CO_ASYNC_GENERATOR) {
PyObject *w = _PyAsyncGenValueWrapperNew(retval);
Py_DECREF(retval);
if (w == NULL) {
retval = NULL;
goto error;
}
retval = w;
}
f->f_state = FRAME_SUSPENDED;
f->f_stackdepth = (int)(stack_pointer - f->f_valuestack);
goto exiting;
}

这个 bytecode 其实非常简单,它就是把栈顶的那个值拿出来存到 retval,然后把当前的这个 frame label 一下,说这个 frame 还没进行完,并且存了一下这个栈现在的深度,然后就退出了。goto exiting,还记得吧?我们之前讲过,就是直接 return value 去了。

所以当你在做 next(g) 的时候,这个函数就在正常地运行。然后到 YIELD_VALUE 的这个地方,它把这个 num 给返回回去了。但是这个函数没有运行完,这个函数相当于被按了一个暂停键。在下一次你再对这个生成器对象使用 next 的时候,它会接着这个 YIELD_VALUE 运行下面的代码。

那如果我们来看 Python 这边代码的话,就更好理解一点。在你做 g = gen(5) 的时候,你把 num 已经赋值好了,但是你没有运行这段函数,而是生成了一个生成器对象。当你运行 next(g) 的时候,它先判断了 5 是不是大于 0。如果是的话,它把 5 先返回,然后这个函数暂停在这块。所以这个时候 first 是 5。

然后下面 for i in g,我们已经说过,对不对?就是在这个 g 里面不断地拿值,每一次相当于都对它 call 了一个 next。那么下一次再 call next 的时候,这个 num 就会减等于 1,那么就会变成 4。然后依然在这个 while loop 里,然后把这个 4 yield 出去。

生成器里的 return​

在进行若干次之后,num 到 0 了,这个时候它会运行这个 return。这里注意一下,由于它是在生成器函数里面了,所以 return 等价于 raise StopIteration。注意啊,这个 return 不管你有没有 return 值,它都不会在你调用 next 的时候被返回回去,只有 yield 值会被存起来。那如果你想拿到这个 return value 的话,你需要去 catch 那个 StopIteration 的 exception。

比如说我这里 return 100,对吧?我这个 for loop 是不会显示出来任何跟 100 有关的事儿的。大家可以看到,从使用者的角度看,生成器和迭代器是没什么区别的,它们的使用方法几乎是一样的。

第 3 步 · return 一个值3 / 5
main.py
def gen(num):
while num > 0:
yield num
num -= 1
return 100
return
g = gen(5)
first = next(g)
for i in g:
print(i)

状态保存在 frame 里​

那从背后的原理看呢,我们正常定义的迭代器,也就是 iterator,它是把这个迭代时候的状态保存在一个对象里的。而生成器,也就是 generator,它实际上是把这个状态保存在了 frame 里。就它的状态是函数运行到哪一步了,而不是通过一个变量来记录我现在的状态。

那生成器这种形式呢,有时候会让你的函数更为简洁。

用生成器遍历链表​

我们还用昨天的链表来举一个例子。我们之前是做了一个链表本身的 iterator,对不对?然后把这个 node 变成了一个 iterable。那这一次我们也是想把这个 node 变成 iterable,但是这个 __iter__ 函数,我们直接把它做成一个生成器。也就是当你做 iter(node) 的时候,它返回的是一个 generator object。但是我们说过,generator object 本身就是一个 iterator,对不对?那这个 generator object 呢,就是从我本身开始,然后 yield 了出去,然后换我后面那个,直到这个 node 是 None 为止。

第 1 步 · 用生成器实现 __iter__1 / 1
main.py
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __iter__(self):
node = self
while node is not None:
yield node
node = node.next
node1 = Node("node1")
node2 = Node("node2")
node3 = Node("node3")
node1.next = node2
node2.next = node3
for node in node1:
print(node.name)

那这种写法看上去,就比我们上一篇文章迭代器的那个写法要简洁非常多。它就是用 frame,包括里面的 local variable,还有它运行到哪儿,来表示状态,而不是用单独的 variable。

我们跑一下这边程序。可以看到啊,结果呢是跟我们之前那篇文章是一样的。那这里呢就体现出了生成器的一个优势:在某些情况,尤其是比较简单的情况下,它的代码量会比传统的迭代器要少不少。

用 send 与生成器沟通​

好,那我们之前说,这个生成器的用法和迭代器几乎是一样的。为什么这加了一个“几乎”呢?因为生成器本身呢,还有一个比较高级的用法,就是 send。那 send 是什么意思呢?send 就是在这个生成器 yield 之后,把这个 yield 什么东西变成一个值,然后这个值还可以继续赋给这个生成器函数里的其他变量。

我们看,我把这个生成器函数稍微改了一下。之前呢就是 yield num,然后 num 减等于一,完事了。这次呢,我把这个 yield num 先赋值给了 tmp,然后检查 tmp 是不是 None。如果 tmp 不是 None 的话,我们就把 num 重新赋值。

第 4 步 · 用 send 改写 num4 / 5
main.py
def gen(num):
while num > 0:
tmp = yield num
if tmp is not None:
num = tmp
yield num
num -= 1
return 100
g = gen(5)
first = next(g) # first = g.send(None)
print(f"first: {first}")
print(f"send: {g.send(10)}")
first = next(g)
for i in g:
print(i)

这个语法可能看着有点怪啊。这里是什么意思呢?就是它首先会先 yield num,就把这个值啊,不管你是用 next 取的,还是用 send 取的,会把这个值返回出去。然后接下来,它会等你 send 的这个值拿进来。你可以理解为这个 yield num 进行了一次 evaluation,evaluate 成了 send 进来的这个值。

我们拿上面这段程序举个例子。first 等于 next(g),这个部分是一样的,先 yield 出来一个 5,所以 first 肯定是 5。然后这个时候呢,我们 g.send(10) 是什么意思呢?就是这个 yield num 的 evaluation,我们认为它是 10,我们把这个 10 给进去了,然后就会把这个 10 赋值给 tmp。接下来 num 就会被赋值成 10,然后 num 再减 1,在下一次 yield num 的时候,出来的就是 9 了。

那你可能会问,对不对?下面这个 for i in g 呢,我们说过,for i in g 相当于是不停地用 next,对不对?那么对于生成器来说,next(g) 和 g.send(None) 是一模一样的。我们看,这里写了一个注释啊,它们两个是完全等价的。所以当你在做 for i in g 的时候,你相当于在不停地做 g.send(None),然后这个 tmp 就会一直被赋值成 None,然后这个 num 就不会改变。

我们运行一下上面这段程序啊,可以看到,跟我们刚才讲的一样。first 是 5,对不对?然后下面那个 send 是 9,因为 num 先被赋值成了 10,然后又减了 1,所以下一次 yield 出来是 9。接下来这个 for loop 就从 8 打到了 1。

我们注意一下,这个生成器跟我们上一个写的生成器,我都没有写 return。没写 return 和最后 return None 是一样的啊。这个在 Python 的任何一个函数里都是这样的。

yield 不赋值时的 send​

那当然了,如果你的生成器里面,本来这个 yield num 只是作为一个所谓 statement 存在,并没有把它赋值给任何东西的时候,你 send 进去的数值就相当于被扔掉了嘛,它不会起作用,但它也不会 break 你这个 generator。

第 5 步 · yield 不赋值时的 send5 / 5
main.py
def gen(num):
while num > 0:
yield num
tmp = yield num
if tmp is not None:
num = tmp
num -= 1
g = gen(5)
first = next(g) # first = g.send(None)
print(f"first: {first}")
print(f"send: {g.send(10)}")
for i in g:
print(i)

那如果这么写的话呢,我们就看,第一个还是 5,对吧?然后你 send 10,就相当于把这个 10 给进去,然后这个 10 被扔掉了,然后 num 还是正常减 1,然后这个值就是 4,对吧?然后后面是 3、2、1。

所以当你这个 yield 什么东西没有把它再赋值给一个变量的时候,你 send 任何东西,包括 send None,跟用 next,就是一样的,是等价的。

send 这个函数呢,只是给你一个机制,让你可以去和你的 generator 做沟通和交流,可以改变它的一些状态。

好了,那这篇有关生成器的文章就讲到这里。希望大家有机会可以尝试一下。