yield & send in python

12 min read
Table of Contents

Python 中 yield 和 send() 的执行过程详解

1. 示例代码

先看这段代码:

python
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)

这段代码演示的是一个简单的 生产者 - 消费者协程模型。

其中:

  • produce() 是生产者,负责生产数据;
  • consumer() 是消费者,负责消费数据;
  • yield 用来暂停消费者;
  • send() 用来恢复消费者,并向消费者传入数据。

2. yield 的基本作用

普通函数遇到 return 就结束了。

例如:

python
def f():
return 1
x = f()
print(x)

输出:

1

但是,如果一个函数里出现了 yield,这个函数就不再是普通函数,而是一个 生成器函数。

例如:

python
def g():
yield 1
yield 2
yield 3

调用它时:

python
gen = g()

函数体不会立刻执行,而是返回一个生成器对象。

只有调用:

python
next(gen)

或者:

python
gen.send(None)

时,函数才会开始执行。


3. yield x 的含义

在生成器中:

python
yield x

表示:

暂停当前函数,并把 x 返回给外部。

例如:

python
def g():
yield 100
gen = g()
result = next(gen)
print(result)

输出:

100

也就是说,yield 后面的值会成为外部调用 next() 或 send() 的返回值。


4. send() 的作用

生成器除了可以用 next() 恢复执行,还可以用 send() 恢复执行。

区别是:

python
next(gen)

只能恢复生成器执行。

而:

python
gen.send(value)

不仅可以恢复生成器执行,还可以把 value 发送到生成器内部。


5. n = yield r 的真正含义

你这段代码里最关键的一句是:

python
n = yield r

这句代码同时包含两个方向:

第一:向外返回 r

python
yield r

表示把 r 返回给外部。

所以外面这句:

python
result = c.send(value)

拿到的返回值,就是 yield r 中的 r。


第二:接收外部 send 进来的值

python
n = yield r

当外部下一次执行:

python
c.send(value)

时,value 会被传回生成器内部,作为 yield r 这个表达式的结果,然后赋值给 n。

也就是说:

python
n = yield r

可以理解成:

python
# 1. 把 r 返回给外部
# 2. 暂停函数
# 3. 等外部 send(value)
# 4. 把 value 赋值给 n

所以:

python
n = yield r

里面:

  • r 是返回给外部的值;
  • n 是接收外部传进来的值。

6. send() 的返回值是不是一直等于 yield 后面的值?

是的,但要说准确一点:

每一次 send() 的返回值,等于生成器恢复执行后遇到的下一个 yield 后面的值。

也就是说:

python
yield x

并不是规定以后 send() 永远返回 x。

而是说:

当前这一次暂停时,生成器把此时的 x 返回给外部。

如果 x 改变了,那么下一次 send() 的返回值也会改变。

例如:

python
def g():
x = 1
while True:
n = yield x
x += 1
gen = g()
print(gen.send(None)) # 1
print(gen.send('a')) # 2
print(gen.send('b')) # 3
print(gen.send('c')) # 4

虽然每次都是:

python
n = yield x

但 x 的值一直在变,所以 send() 的返回值也在变。


7. 为什么第一次要 c.send(None)?

在你的代码中:

python
c = consumer()

这一步只是创建了生成器对象。

此时 consumer() 内部代码还没有开始执行。

也就是说,这时候还没有执行到:

python
n = yield r

生成器还没有停在任何一个 yield 上。

所以第一次启动生成器时,必须先用:

python
c.send(None)

或者:

python
next(c)

它们的作用类似,都是让生成器运行到第一个 yield 并暂停。

如果第一次直接写:

python
c.send(1)

会报错:

TypeError: can't send non-None value to a just-started generator

原因是:

生成器还没有运行到第一个 yield,所以没有地方接收你 send 进去的值。


8. 第一次 c.send(None) 做了什么?

看 consumer():

python
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

第一次执行:

python
c.send(None)

生成器开始运行:

python
r = ''

然后进入循环:

python
while True:

执行到:

python
n = yield r

此时 r 是空字符串:

python
r = ''

所以第一次 yield r 返回的是:

python
''

但是在你的代码中:

python
c.send(None)

没有用变量接收这个返回值,所以这个空字符串被忽略了。

如果写成:

python
first = c.send(None)
print(first)

那么会打印一个空字符串。


9. 第一次正式发送数据:c.send(1)

生产者代码中:

python
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)

第一次循环时:

python
n = 1

所以执行:

python
r = c.send(1)

这时候消费者之前停在这里:

python
n = yield r

现在外部发送了 1 进来。

于是:

python
n = yield r

中的 yield r 这个表达式的结果就是 1。

所以消费者内部变成:

python
n = 1

然后继续执行:

python
if not n:
return

因为 n = 1,所以不会 return。

接着执行:

python
print('[CONSUMER] Consuming %s...' % n)

打印:

[CONSUMER] Consuming 1...

然后:

python
r = '200 OK'

接着循环继续,又回到:

python
n = yield r

此时 r 已经变成了:

python
'200 OK'

所以这次 yield r 会把:

python
'200 OK'

返回给外面的:

python
r = c.send(1)

因此生产者中的 r 得到的值就是:

python
'200 OK'

所以打印:

[PRODUCER] Consumer return: 200 OK

10. 完整执行流程

创建生成器

python
c = consumer()

此时只是创建生成器,consumer() 内部代码还没有执行。


启动生成器

python
c.send(None)

消费者执行到:

python
n = yield r

此时:

python
r = ''

所以第一次返回空字符串,并暂停。


生产者生产 1

python
print('[PRODUCER] Producing 1...')
r = c.send(1)

输出:

[PRODUCER] Producing 1...

消费者收到:

python
n = 1

然后消费数据:

[CONSUMER] Consuming 1...

消费完成后:

python
r = '200 OK'

然后再次执行到:

python
n = yield r

把 '200 OK' 返回给生产者。

生产者拿到返回值后打印:

[PRODUCER] Consumer return: 200 OK

生产者生产 2

python
print('[PRODUCER] Producing 2...')
r = c.send(2)

输出:

[PRODUCER] Producing 2...

消费者收到:

python
n = 2

打印:

[CONSUMER] Consuming 2...

然后再次返回:

python
'200 OK'

生产者打印:

[PRODUCER] Consumer return: 200 OK

后续 3、4、5 也是一样

每次流程都是:

生产者生产一个数字
send 给消费者
消费者消费这个数字
消费者把 r 设置为 '200 OK'
下一次 yield r
生产者拿到 '200 OK'

11. 最终输出结果

这段代码最终大致输出:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意:第一次 c.send(None) 返回的是空字符串,但是代码没有打印它。


12. 用表格理解执行过程

步骤外部调用发送给消费者的值消费者内部 n消费者 yield 返回值外部收到的返回值
1c.send(None)None暂未赋值'''',但被忽略
2c.send(1)11'200 OK''200 OK'
3c.send(2)22'200 OK''200 OK'
4c.send(3)33'200 OK''200 OK'
5c.send(4)44'200 OK''200 OK'
6c.send(5)55'200 OK''200 OK'

13. 关键点总结

yield r

表示:

把 r 返回给外部,然后暂停函数。

c.send(n)

表示:

把 n 发送进生成器,并恢复生成器继续执行。

r = c.send(n)

表示:

把 n 发给生成器;
生成器运行到下一次 yield;
yield 后面的值作为 c.send(n) 的返回值;
赋值给外部变量 r。

n = yield r

这是双向通信:

r:返回给外部;
n:接收外部 send 进来的值。

所以这句可以记成:

外面拿到 r;
里面收到 n。

14. 为什么后面的返回值一直是 200 OK?

因为消费者每次消费完数据后,都会执行:

python
r = '200 OK'

然后循环回到:

python
n = yield r

此时 yield r 中的 r 就是:

python
'200 OK'

所以外部:

python
r = c.send(n)

每次拿到的返回值都是:

python
'200 OK'

但是它不是因为 send() 固定返回 '200 OK'。

而是因为消费者内部每次都把 r 设置成了 '200 OK'。

如果你改成:

python
r = f'{n} consumed'

那么返回值就会变化。

例如:

python
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = f'{n} consumed'

那么输出会变成:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 1 consumed
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 2 consumed
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 3 consumed

15. 如果发送的是 0 会怎么样?

消费者中有这段代码:

python
if not n:
return

在 Python 中,下面这些值都会被认为是 False:

python
0
None
''
False
[]
{}

所以如果执行:

python
c.send(0)

那么:

python
n = 0

接着:

python
if not n:
return

条件成立,消费者结束。

生成器结束后,会抛出:

StopIteration

所以:

python
c.send(0)

不会正常返回 '200 OK',而是终止生成器。


16. close() 的作用

生产者最后调用:

python
c.close()

表示关闭生成器。

关闭之后,生成器不能再继续执行。

如果关闭后再调用:

python
c.send(1)

会抛出:

StopIteration

17. 最重要的一句话

这句代码:

python
n = yield r

可以记成:

yield 后面的 r,是返回给外面的值;
send 进来的值,会赋给里面的 n。

所以:

python
r = c.send(n)

可以理解成:

外部把 n 发送进去;
内部接收为 n;
内部处理完后 yield r;
外部拿到 r。

这就是 yield + send() 实现协程通信的核心。

后记

My avatar

感谢你读到这里。

这座小站更像一份持续维护的“终端笔记”:记录我解决问题的过程,也记录走过的弯路。


More Posts

评论

评论 (0)

请先登录后再发表评论

加载中...