yield笔记

看到协程时对yield的用法总是理解不够透彻,因此做一些小笔记,方便日后查看。
此处以一个小例子来说明send到底是干嘛用的,例子来自Python协程:从yield/send到async/await:

1、起源:简单的yield生成器

def fib(n):
    index = a = 0
    b = 1
    while index<n:
        yield b
        a, b = b, a+b
        index += 1

#简单调用:
for i in fib(5):
    print(i,end=' ',sep=',')
# 1 1 2 3 5

#相当于
f = fib(5)
for j in range(5):
    print(next(f),end=' ')

在上面的例子中,函数fib(n)相当于一个生成器,for循环每一次调用相当于执行一次next(),最后调用完会遇到StopIteration退出for循环。

2、send是什么?

想要了解send()是什么,就不得不先理解yield表达式了。yield表达式可表示为[res =] yield [expression],从某种程度来说,方括号中的值都是可以省略的,例如若将fib(n)函数的yield n改成yield也不会报错,只是这样fib(5)这个生成器就会返回5个None了,而将yield n改成 s = yield n结果则不会变,只是此时可以通过sendyield表达式传入数值,这个数值即赋值给了s。看看代码更加清晰:

# yield b ==> yield
def fib(n):
    index = a = 0
    b = 1
    while index<n:
        yield
        a, b = b, a+b
        index += 1

for i in fib(5):
    print(i,end=' ')
# None None None None None

# yield b ==> s=yield b
def fib(n):
    index = a = 0
    b = 1
    while index<n:
        s = yield b
        a, b = b, a+b
        index += 1
# 1 1 2 3 5

既然使用了yield表达式后对生成器没有改变,那么他有什么作用呢?要想yield表达式发挥作用,就必须使用send对其进行传值(赋值),利用传入的值可以来实现一些有用的功能,例如下面简单的记录一下日志信息:

import datetime
import time
import random


def fib(n):
    index = a = 0
    b = 1
    while index < n:
        now = yield b
        print(now)
        a, b = b, a+b
        index += 1

f = fib(5)
res = next(f)   #这一步是必须的,此处相当于send(None),在fib函数中此时执行到yield产出值b=1(也即res等于1),并挂起等待send传入值
while True:
    try:
        print(res)
        time.sleep(random.random())
        res = f.send(datetime.datetime.now())
    except:
        print('over')
        break
#输出
1
2017-12-21 14:15:49.296765
1
2017-12-21 14:15:49.583339
2
2017-12-21 14:15:50.492255
3
2017-12-21 14:15:51.006442
5
2017-12-21 14:15:51.113537
over

从上面可以看出,send发送的值都赋值给了yield表达式的左边的now变量了。另外值得注意的一点是,在使用send之前必须先调用一次next(),此处的next相当于send(None)
yield表达式的执行顺序是先yield产生值,然后挂起等待send传入值。也因此输出的结果是先输出fib序列,然后在输出传入值相关的信息。
下面的例子更好的说明了执行步骤,为了更好的说明执行顺序,此处将fib序列的第一个值改成了2:


import datetime
import time
import random


def fib(n):
    index = 0
    a = 2
    b = 3
    while index < n:
        now = yield b
        print(now)
        a, b = b, a+b
        index += 1

f = fib(5)
res = next(f)
n = 1
while n < 2:
    try:
        print(res)
        time.sleep(random.random())
        res = f.send(datetime.datetime.now())
        print(res)
    except:
        print('over')
        break
    n += 1
#此时仅执行了一次send,输出如下
3
2017-12-21 21:23:06.106728
5

上面的两个不同的fib生成结果中第一个3是在预激活协程时yield产生的,在yield表达式右边产生值后,便会挂起等待传入参数并赋值给左侧的变量now。随后send将时间传入赋值给了yield 表达式左边的nownow被赋值后会一直执行到再次yield b生成5,这也是为什么下面的res是5。此时yield 表达式又再次执行到了yield并挂起等待给now赋值(send)的时候。如此循环,直到yield表达式右侧(这里的右侧依旧是一个生成器)的值耗尽,这是再次send时会引发生StopIteration

3、yield from 是何方神圣?

说完了sendyield from又是用来干什么的呢?下面这个例子也许可以对yield from的用法做一些最简单的说明:

def f1():
    for  i in range(5):
        yield i
    for j in 'abc':
        yield j

def f2():
    yield from f1()

# 下面两种调用生成器的结果是一致的
for i in f1():
    print(i,end=' ')
for j in f2():
    print(j,end=' ')
# 0 1 2 3 4 a b c

但是yield from的作用仅仅如此吗?
你见过有返回值的生成器吗?下面这个生成器在终止时(触发StopIteration)会返回一个值。

def func():
    index = 0
    res = 111
    while index < 5:
        s = yield  # ④
        print('s: ', s)  
        index += 1
    return res

def delegate():
    res = yield from func()  # ③         ##⑥
    print('res: ', res)

f = delegate()
f.send(None)     # ①
i = 10
while i < 15:
    try:
        f.send(i)  # ② 此处send的值发送到了子生成器func()中
    except:
        pass
    i += 1
#输出
s:  10
s:  11
s:  12
s:  13
s:  14
res:  111  

从输出结果可以看出3件事:
首先,委派生成器delegate的确从yield from中收到了返回值——res=111,而且这个返回值并非yield的值,而是子生成器函数func的返回值
其次,从输出结果可以看出,send发送的值都传到了子生成器中,也即是从委派生成器delegate传到了yield from表达式中的子生成器func中,这也是输出结果index 10 ... index 14的由来。
最后,委派生成器向子生成器send发送值后,自身会被挂起,直到子生成器函数func触发终止异常(StopIteration)返回值,这个返回值赋值给yield from表达式左边的res,然后委派生成器就会继续执行,这也是为什么,res:111会在最后输出。当我们将while i<15改成while i<12后会看到输出结果没有res输出,这是因为生成器还没有迭代完(while index<5这里需要send5次才会触发异常返回值),还在等待send发送值。

现在,让我们梳理下上面代码的执行顺序:
①预激活子生成器func,此时子生成器在等待传值;
②向委派生成器delegate传值(send);
③委派生成器通过yield from表达式向子生成器func中传(send)值;
④子生成器收到传入的值i后,便会向后执行print('s: ', s)index+=1yield产生值(虽然此处没有生成任何值),最后又回到继续等待传值(send)的状态;
⑤代码中没有⑤因为⑤代表着循环传值这个过程;
终止生成器,这一步非常关键,因为每传入(send)一个值后都会执行index+=1,回到等待传值得状态。因此第一次传值后index=1(需要注意的是预激活时index为0),第四次传值(send(13))后,此时index=4,当第五次传值时index=5此时会触发异常退出while循环,使得子生成器func返回res值,func返回的值又通过yield from表达式赋值给委派生成器的resres收到值后委派生成器终于不再挂起,向下执行,print(res)。完结撒花。

推荐阅读更多精彩内容

  • 从语法上来看,协程和生成器类似,都是定义体中包含yield关键字的函数。yield在协程中的用法:在协程中yiel...
    JokerW阅读 1,603评论 0 0
  • 1.1==,is的使用 ·is是比较两个引用是否指向了同一个对象(引用比较)。 ·==是比较两个对象是否相等。 1...
    TENG书阅读 383评论 0 0
  • 你不知道JS:异步 第四章:生成器(Generators) 在第二章,我们明确了采用回调表示异步流的两个关键缺点:...
    purple_force阅读 662评论 0 2
  • 文/南下溪 海子说:“我们最终都要远行,都要与稚嫩的自己告别通向苦行之路。”有人选择按部就班地承受生活的煎熬,有人...
    南下溪阅读 9,286评论 196 670
  • 小可爱
    三青w阅读 108评论 0 0