yield全面总结

本文首发于知乎
yield生成器在python中使用广泛,更是python中协程的实现原理,有必要深入掌握。

本文分为如下几个部分

  • 简单yield的使用
  • yield空
  • yield from
  • send与yield赋值
  • return yield

简单yield的使用

下面是一个最简单的yield使用的例子

def myfun(total):
for i in range(total):
yield i
复制代码

定义了myfun函数,调用时比如myfun(4)得到的是一个生成器,生成器有3种调用方式

1.用next调用

>>> m = myfun(4)
>>> next(m)
0
>>> next(m)
1
>>> next(m)
2
>>> next(m)
3
>>> next(m)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
复制代码

每次调用一次next,函数都会执行到yield处停止,等待下一次nextnext次数超出限制就会抛出异常

2.循环调用

for i in myfun(4):
print(i)
复制代码

运行结果为

0
1
2
3
复制代码

生成器是可迭代对象,可以用循环调用。循环调用就是最大限度地调用next,并返回每次next运行结果

这就是yield的最基本使用,下面来应用一下

python内置模块itertools中实现了一些小的生成器,我们这里拿count举例子,要实现下面这样的效果

>>> from itertools import count
>>> c = count(start = 5, step = 2)
>>> next(c)
5
>>> next(c)
7
>>> next(c)
9
>>> next(c)
11
......
复制代码

实现如下

def count(start, step = 1):
n = 0
while True:
yield start + n
n += step
复制代码

3.循环中调用next

下面循环中调用next,用StopIteration捕获异常并退出循环

>>> m = myfun(4)
>>> while True:
... try:
... print(next(m))
... except StopIteration:
... break
...
0
1
2
3
复制代码

yield空

yield空相当于一个中断器,循环运行到这里会中断,用于辅助其他程序的执行。也可以理解成返回值是None,我们来看下面这个例子

>>> def myfun(total):
... for i in range(total):
... print(i + 1)
... yield
...
>>> a = myfun(3)
>>> next(a)
1
>>> next(a)
2
>>> next(a)
3
>>> next(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> a = myfun(3)
>>> for i in a:
... print(i)
...
1
None
2
None
3
None
复制代码

yield from

通过下面一个例子来展示yield from的用法

def myfun(total):
for i in range(total):
yield i
yield from ['a', 'b', 'c']
复制代码

next调用结果如下

>>> m = myfun(3)
>>> next(m)
0
>>> next(m)
1
>>> next(m)
2
>>> next(m)
'a'
>>> next(m)
'b'
>>> next(m)
'c'
>>> next(m)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
复制代码

上面的函数相当于

def myfun(total):
for i in range(total):
yield i
for i in ['a', 'b', 'c']:
yield i
复制代码

下面我们也做一个小练习,实现itertools模块中的cycle,效果如下

>>> from itertools import cycle
>>> a = cycle('abc')
>>> next(a)
'a'
>>> next(a)
'b'
>>> next(a)
'c'
>>> next(a)
'a'
>>> next(a)
'b'
>>> next(a)
'c'
>>> next(a)
'a'
复制代码

实现如下

def cycle(p):
yield from p
yield from cycle(p)
复制代码

send与yield赋值

先讲send,首先明确一点,next相当于send(None),还是看下面最简单的例子

>>> def myfun(total):
... for i in range(total):
... yield i
...
>>> a = myfun(3)
>>> a.send(None)
0
>>> a.send(None)
1
>>> a.send(None)
2
>>> a.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
复制代码

如果send()参数不是None呢?send()表示向这个生成器中传入东西,有传入就得有变量接着,于是引出了yield赋值,来看下面一个例子

def myfun(total):
for i in range(total):
r = yield i
print(r)
复制代码

运行如下

>>> a = myfun(3)
>>> a.send(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> a.send(None)
0
>>> a.send(1)
1
1
>>> a.send(1)
1
2
>>> a.send(1)
1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
复制代码

上面运行结果显示

1.第一次send参数必须是None,用于初始化生成器

  1. 整个程序运行细节非常重要,详细拆解如下
  • 第一步a.send(None),是执行第一次循环,运行到yield i。注意:没有运行r = yield赋值这一步,这也是第一次只能传入None的原因
  • 第二步a.send(1),先r = yield赋值,将send进去的1赋值给了r;然后print(r)即第一个打印出来的1;之后进入第二次循环,运行到yield i没有赋值,把yield的结果1打印出来,即第二个1
  • 最后一次a.send(1)先对r赋值,再print(r);然后就退出了循环,没有什么可以yield的了,于是抛出异常

有了send这样的机制,我们就可以实现函数之间的来回切换执行,这是协程的基础。

廖雪峰老师网站上用这一特性完成了一个类似生产者消费者模式的例子,读者可以看看根据上面的知识能不能看懂这个例子

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)
复制代码

运行结果如下

[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
复制代码

上面例子看起来复杂高大上,但是如果从实现上述功能上来看,实际上是走了弯路,下面的代码可以实现和上面一样的功能,读者可以细细品味

def consumer(n):
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
return '200 OK'
def produce():
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = consumer(n)
print('[PRODUCER] Consumer return: %s' % r)
produce()
复制代码

return yield

有时会看到return yield的用法,其实return只是起到终止函数的作用,先看下面这个函数

def myfun(total):
yield from range(total)
复制代码
>>> a = myfun(4)
>>> a
<generator object myfun at 0x000001B61CCB9CA8>
>>> for i in a:
... print(i)
...
0
1
2
3
复制代码

这样a就是个生成器。如果用return也一样

# 不加括号不合规定
>>> def myfun(total):
... return yield from range(total)
File "<stdin>", line 2
return yield from range(total)
^
SyntaxError: invalid syntax
>>> def myfun(total):
... return (yield from range(total))
...
>>> a = myfun(4)
>>> for i in a:
... print(i)
...
0
1
2
3
复制代码

不过下面两个不一样

def myfun1(total):
return (yield from range(total))
yield 1 # 这个1在return后面,不会再执行
def myfun2(total):
yield from range(total)
yield 1
复制代码

看下面运行结果

>>> a = myfun1(3)
>>> for i in a:
... print(i)
...
0
1
2
>>> a = myfun2(3)
>>> for i in a:
... print(i)
...
0
1
2
1
复制代码

欢迎关注我的知乎专栏

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值