Skip to content

Latest commit

 

History

History
782 lines (542 loc) · 21.5 KB

3.yield from详解.md

File metadata and controls

782 lines (542 loc) · 21.5 KB

yield from 详解

1.yield from的简单实现

从前面的系列文章中,我们了解到,yield是每次“惰性返回”一个值,其实从名字中就能看出,yield from 是yield的升级改进版本,如果将yield理解成“返回”,那么yield from就是“从什么(生成器)里面返回”,这就构成了yield from的一般语法

yield from generator

demo:

def generator2():
    yield 'a'
    yield 'b'
    yield 'c'
    yield from [11, 22, 33, 44]
    yield from (12, 23, 34)
    yield from range(3)


for i in generator2():
    print(i, end=' , ')

运行结果:

a,b,c,11,22,33,44,0,1,2

总结:生成器 、元组、 列表、range()函数产生的序列等可迭代对象返回另外一个生成器。而yield只是返回一个元素。从这个层面来说,有下面的等价关系

yield from iterable 本质上等于

for item in iterable: 
    yield item

2.yield from的高级应用

2.1针对yiled无法获取生成器return的返回值

我们都知道,在使用yield生成器的时候,如果使用for语句去迭代生成器,则不会显式的触发StopIteration异常,而是自动捕获StopIteration异常,所以如果遇到return,只是会终止迭代,而不会触发异常,故而也就没办法获取return的值。如下:

def my_generator():
    for i in range(5):
        if i == 2:
            return '我被迫中断了'
        else:
            yield i


def main(generator):
    try:
        for i in generator:  # 不会显式触发异常,故而无法获取到return的值
            print(i)
    except StopIteration as exc:
        print(exc.value)


g = my_generator()  # 调用
main(g)

运行结果:

0
1

从上面的例子可以看出,for迭代语句不会显式触发异常,故而无法获取到return的值,迭代到2的时候遇到return语句,隐式的触发了StopIteration异常,就终止迭代了,但是在程序中不会显示出来。

def my_generator():
    for i in range(5):
        if i == 2:
            return '我被迫中断了'
        else:
            yield i


def main(generator):
    try:
        print(next(generator))  # 每次迭代一个值,则会显式出发StopIteration
        print(next(generator))
        print(next(generator))
        print(next(generator))
        print(next(generator))
    except StopIteration as exc:
        print(exc.value)  # 获取返回的值


g = my_generator()
main(g)

运行结果:

0
1
我被迫中断了

现在我们使用yield from来完成上面的同样的功能:

def my_generator():
    for i in range(5):
        if i == 2:
            return '我被迫中断了'
        else:
            yield i


def wrap_my_generator(generator):  # 定义一个包装“生成器”的生成器,它的本质还是生成器
    result = yield from generator  # 自动触发StopIteration异常,并且将return的返回值赋值给yield from表达式的结果,即result
    print(result)


def main(generator):
    for j in generator:
        print(j)


g = my_generator()
wrap_g = wrap_my_generator(g)
main(wrap_g)  # 调用

运行结果:

0
1
我被迫中断了

从上面的比较可以看出,yield from具有以下几个特点:

(1)上面的my_generator是原始的生成器,main是调用方,使用yield的时候,只涉及到这两个函数,即“调用方”与“生成器(协程函数)”是直接进行交互的,不涉及其他方法,即**“调用方——>生成器函数(协程函数)”**;

(2)在使用yield from的时候,多了一个对原始my_generator的包装函数,然后调用方是通过这个包装函数(后面会讲到它专有的名词)来与生成器进行交互的,即**“调用方——>生成器包装函数——>生成器函数(协程函数)”;**

(3)yield from iteration结构会在内部自动捕获 iteration生成器的StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样。而且对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把return返回的值或者是StopIteration的value 属性的值变成 yield from 表达式的值,即上面的result。

2.2 yield from所实现的数据传输通道

前面总结的几个特点里面已经介绍了yield和yield from的数据交互方式,yield涉及到“调用方与生成器两者”的交互,生成器通过next()的调用将值返回给调用者,而调用者通过send()方法向生成器发送数据;

但是yield还有一个第三者函数,下面将先从相关的概念说起。

在PEP 380 使用了一些yield from使用的专门术语:

委派生成器: 包含 yield from iterable 表达式的生成器函数;即上面的wrap_my_generator生成器函数

子生成器: 从 yield from 表达式中 iterable 部分获取的生成器;即上面的my_generator生成器函数

调用方: 调用委派生成器的客户端代码;即上面的main生成器函数

下图是这三者之间的交互关系:

img

委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复。

2.2.1 系统模型-从底层到上层的传递的协程

假设我们有一个系统, 它的业务需求是这样:

  1. 需要读取一段放在一个常量列表中的文本, 每个item表示一行文本。

  2. 每读入一行,则先打印双小于号 "<<",然后打印读入的文本行

  3. 如果文本全部成功读取,则最后打印“done”表示应用正常结束

系统最初版本,由一个reader和app实现,转化的软件需求分别如下:

  1. reader是一个generator。

   1)用来模拟文本读取,每次读取text中一行,并返回该行文本

​ 2)如果文本全部读完,返回‘done’

  1. app是主线业务, 软件需求如下:

    1)while循环中,每次通过next读取reader一个生成的值,并在文本前加“<<"后打印

    2)如果reader产生StopIteration异常, 则打印reader的返回值

这个初版用python实现如下:

demo:

# reader是一个生成器, 每次调用,它将读取列表中一个值并返回
def reader(text):
    for line in text:
        yield line
    return 'done'


# app是定义的一个简单应用,将reader读出的值打印出来
def app(text):
    try:
        r = reader(text)
        while True:
            line = next(r)
            print('<< %s' % line)
    except StopIteration as e:
        print(e.value)


# 启动app应用
app(('a', 'b', 'c', 'd'))

运行结果:

<< a
<< b
<< c
<< d
done

2.2.2 新需求引入

现在系统需求改变了,在文件开始读取和结束读取之后,需要记录日志以满足运维需求,但系统原有业务无关,是和主线业务正交的非业务需求。为了避免以后再次修改app应用,引入一个代理proxyReader处理这些切面类需求。

1)Reader只涉及底层文本读取,和之前一样。

2)定义前后两个方法。 运维需求省略具体实现,并且抽象成一个before和after方法,里面可以添加记录日志等运维相关的需求

def before():   # 文件开始处理之前的额外处理,比如记录日志
    pass  
    
def after():    # 文件读取完成之后的额外处理,比如记录日志
    pass   
  1. app的改动:通过代理从reader获取值, 不再直接使用reader,这样所有和app无关的运维相关需求都可以在代理中实现, 而且app和reader均无再做任何修改,所有运维需求带来的改动以后都被将封装在代理中
def app(text):    
    try:
        r = proxyReader(text)
        while True:
            line = next(r)
            print('<< %s' % line)
		except StopIteration as e:
            print(e.value)

4)代理生成器的软件需求。

4.1 不能对reader的产生的值做任何修改,原样上报给app

4.2 需要处理新加的非主线的运维需求

def proxyReader(text):
    before()      #开始读之前运维需求处理
    r = reader(text)
    while True:
        line = next(r)
        yield '<< %s' % line
    after()       #结束读之后运维需求处理

完整demo:

def before():
    print("文本开始处理前需要做的一些事情")


def after():
    print("文本结束处理后需要做的一些事情")


# reader是一个生成器,不变化
def reader(text):
    for line in text:
        yield line
    return 'done'


# 添加一个代理
def proxyReader(text):
    before()  # 开始读之前运维需求处理
    r = reader(text)
    while True:
        line = next(r)
        yield '<< %s' % line
    after()  # 结束读之后运维需求处理


# app是定义的一个简单应用,将reader读出的值打印出来
def app(text):
    try:
        r = proxyReader(text)
        while True:
            line = next(r)
            print('<< %s' % line)
    except StopIteration as e:
        print(e.value)


# 启动app应用
app(('a', 'b', 'c', 'd'))

运行结果:

文本开始处理前需要做的一些事情
<< << a
<< << b
<< << c
<< << d
Traceback (most recent call last):
    line = next(r)
StopIteration: done

问题的引入:

结果不是我们所预期的, 出了两个问题:

  1. 每行文本都多打印了两个小于符号"<< "

  2. ‘done’虽然打印出来了,但是仍然产生一个StopIteration异常

下面我们来分析这两个错误的原因,并解决。

第一个错误,是我们的在编写proxyReader的时候,误读了需求,把主线业务需求(在每行前面新加”<< "当成了proxyReader的需求,结果导致重复打印)

第二个错误,是由于proxyReader在使用reader的时候没有处理StopIteration, 而app处理的是proxyReader的StopIteration. 所以导致输出结果有异常日志

这两个错误的根本原因其实是一个*,*为了将包含yield的业务代码从app中分离出去, proxyReader采用的是用yield来转换subgenerator的数据到app,由于这个转换操作必须由proxyReader自己实现,所以它实际割裂了真正的reader和caller(也就是app)之间的数据上传通道。下面是基于yield的修改版本,正确实现数据转换和传送:

修改后demo:

def before():
    print("文本开始处理前需要做的一些事情")


def after():
    print("文本结束处理后需要做的一些事情")


# reader是一个生成器,不变化
def reader(text):
    for line in text:
        yield line
    return 'done'


# 添加一个代理
def proxyReader(text):
    before()  # 开始读之前运维需求处理
    r = reader(text)
    try:
        while True:
            line = next(r)
            yield line
    except StopIteration as e:
        return e.value
    
    after()  # 结束读之后运维需求处理


# app是定义的一个简单应用,将reader读出的值打印出来
def app(text):
    try:
        r = proxyReader(text)
        while True:
            line = next(r)
            print('<< %s' % line)
    except StopIteration as e:
        print(e.value)


# 启动app应用
app(('a', 'b', 'c', 'd'))

运行结果:

文本开始处理前需要做的一些事情
<< a
<< b
<< c
<< d
done

2.2.3 yield from 解决

上面的错误,根本原因是proxyReader割裂了reader与app的联系,靠程序员人工保证很不靠谱, 所以这是python3.3引入yield from的真正动力所在, yield from的解决方案如下:

def before():
    print("文本开始处理前需要做的一些事情")


def after():
    print("文本结束处理后需要做的一些事情")


# reader是一个生成器,不变化
def reader(text):
    for line in text:
        yield line
    return 'done'


# 添加一个代理
def proxyReader(text):
    before()  # 开始读之前运维需求处理
    result = yield from reader(text)
    after()  # 结束读之后运维需求处理
    return result


# app是定义的一个简单应用,将reader读出的值打印出来
def app(text):
    try:
        r = proxyReader(text)
        while True:
            line = next(r)
            print('<< %s' % line)
    except StopIteration as e:
        print(e.value)


# 启动app应用
app(('a', 'b', 'c', 'd'))

这是yield from真正牛逼的地方, 它使proxyReader无需在关心reader与app之间数据通道,这个数据通道被yield from完全封装,对proxyReader透明,而且proxyReader完全无权干涉, 也不可能在有马大哈式的重复打印, reader的输出是啥, yield from百分百保证了app收到的就是啥。

2.2.4 系统模型--从上层到底层的传递的协程

同样,仍然是上文的系统, 指把结束操作改为支持空行操作,它的业务需求是这样:

  1. 需要读取一段放在一个常量列表中的文本, 每个item表示一行文本, 空行用空字符串''表示。

  2. 每读入一行,如果不是空行,则先打印双小于号 "<<",然后打印读入的文本行

  3. 如果读入是空行,则打印"Error: Empty Line"

系统最初版本,这次我们把实现方式稍微变下,改成由一个writer和app实现,数据传输方向从上篇的reader到app, 变成app到writer。软件需求分别如下

  1. writer是一个协程coroutine。

  1)用来模拟文本接收,每次接收一行

​ 2)收到文本后,先打印">>" 然后打印收到的文本

​ 3)如果接收时产生EmptyException异常, 则打印"Error: Empty Line"

  1. app是主线业务, 软件需求如下:

    1)while循环中,每次通过读取text列表中的一个item

    2)如果读到的不是空行,则直接将文本通过send发送给writer

    1. 如果读到的是空行,则触发writer产生EmptyException异常

这个初版用python实现如下:

# 1. 需要读取一段放在一个常量列表中的文本, 每个item表示一行文本, **空行用空字符串''表示。**
# 2. 每读入一行,如果不是空行,则先打印双小于号 "<<",然后打印读入的文本行
# 3. 如果读入是空行,则打印"Error: Empty Line"


# 定义一个EmptyException异常
class EmptyException(Exception):
    pass


# 1. writer是一个协程coroutine。
#   1)用来模拟文本接收,每次接收一行
#    2)收到文本后,先打印">>" 然后打印收到的文本
#    3)如果接收时产生EmptyException异常, 则打印"Error: Empty Line"
# reader是一个协程, 它循环等待接收一行文本并打印输出

def writer():
    while True:
        try:
            t = yield
            print('>> %s' % t)
        except EmptyException:
            print('Error: Empty Line')


# 2. app是主线业务, 软件需求如下:
#     1)while循环中,每次通过读取text列表中的一个item
#     2)如果读到的不是空行,则直接将文本通过send发送给writer
#     3) 如果读到的是空行,则触发writer产生EmptyException异常
# app是定义的一个简单应用,将reader读出的值打印出来
def app(text):
    w = writer()
    w.send(None)  # 激活writer
    for line in text:
        if line == '':
            w.throw(EmptyException)
        else:
            w.send(line)


# 启动app应用
app(('a', 'b', '', 'c'))

运行结果:

>> a
>> b
Error: Empty Line
>> c

2.2.5 新需求引入

现在系统需求改变了,在文件开始输出之前,需要记录日志以满足运维需求,运维需求和原有业务无关。为了避免以后再次修改app应用,引入一个代理proxyWriter处理这些切面类需求。

1)Writer只涉及底层文本打印输出,和之前一样。

2)定义一个before方法。 运维需求省略具体实现,里面可以添加记录日志等运维相关的需求

  1. app的改动:将要输出的文本发送给代理proxyWriter, 不再直接发送给writer,这样所有和app无关的运维相关需求都可以在代理中实现, 而且app和writer均无再做任何修改,所有运维需求带来的改动以后都被将封装在代理中。
# app是定义的一个简单应用,将text读出, 并逐行发送给writer
def app(text):    
    w = proxyWriter()            
    w.send(None)            #激活proxyWriter
    for line in text:
        if line=='':
            w.throw(EmptyException)
        else:
            w.send(line)

4)代理proxyWriter的软件需求。

4.1)不能对app传给writer的值做任何修改,原样下发给writer

4.2)被app触发的异常EmptyException, 也需要下发给writer

4.3) 需要处理新加的非主线的运维需求

# proxyWriter是一个代理,它循环等待接收app发送的一行文本,并转发给writer, 自己不做任何文本处理
def proxyWriter():
    before()                #处理运维相关需求
    w = writer()            
    w.send(None)            #激活writer
    while True:
        t = (yield)
        w.send ('>> %s' % t)

全部代码:

# 定义一个EmptyException异常
class EmptyException(Exception):
    pass


def before():
    print("文本开始处理前需要做的一些事情")


def after():
    print("文本结束处理后需要做的一些事情")


# 子生成器
def writer():
    while True:
        try:
            t = yield
            print('>> %s' % t)
        except EmptyException:
            print('Error: Empty Line')


# 代理
def proxyWriter():
    before()
    w = writer()
    w.send(None)  # 启动生成器
    while 1:
        t = yield
        w.send('>> %s' % t)


def app(text):
    w = proxyWriter()
    w.send(None)  # 激活writer
    for line in text:
        if line == '':
            w.throw(EmptyException)
        else:
            w.send(line)


# 启动app应用
app(('a', 'b', '', 'c'))

运行结果:

文本开始处理前需要做的一些事情
>> >> a
>> >> b
Traceback (most recent call last):
    app(('a', 'b', '', 'c'))

2.2.6 问题的引入

结果不是我们所预期的, 出了两个问题:

  1. 每行文本都多打印了两个小于符号">> "

  2. EmptyException没有被正确处理

下面我们来分析这两个错误的原因,并解决。

第一个错误,是我们的在编写proxyWriter的时候,误读了需求,把writer的业务需求(在每行前面新加”>>") 当成了proxyWriter的需求,结果导致重复打印

第二个错误,是由于proxyWriter在向writer发消息时,没有处理EmptyException, 导致writer虽然有EmptyEception处理逻辑,但由于代理没把异常下传, 导致处理遗漏

这两个错误的根本原因其实是一个*,*为了将包含yield的业务代码从app中分离出去, proxyWriter采用的是用Send来转发app到writer的数据,由于这个转发操作必须由proxyWriter自己实现,所以它实际割裂了app和实际writer的数据下方通道。下面是基于send的修改版本,正确实现数据转换和传值

# 定义一个EmptyException异常
class EmptyException(Exception):
    pass


def before():
    print("文本开始处理前需要做的一些事情")


def after():
    print("文本结束处理后需要做的一些事情")


# 子生成器
def writer():
    while True:
        try:
            t = yield
            print('>> %s' % t)
        except EmptyException:
            print('Error: Empty Line')


# 代理
def proxyWriter():
    before()
    w = writer()
    w.send(None)  # 启动生成器
    while 1:
        try:
            t = yield
            w.send(t)  # 接收app的数据后,需要转发到writer
        except EmptyException:
            w.throw(EmptyException)  # 处理app下发的EmptyException, 也需要下发到writer


def app(text):
    w = proxyWriter()
    w.send(None)  # 激活writer
    for line in text:
        if line == '':
            w.throw(EmptyException)
        else:
            w.send(line)


# 启动app应用
app(('a', 'b', '', 'c'))

运行结果:

文本开始处理前需要做的一些事情
>> a
>> b
Error: Empty Line
>> c

2.2.7 yield from 解决

# 定义一个EmptyException异常
class EmptyException(Exception):
    pass


def before():
    print("文本开始处理前需要做的一些事情")


def after():
    print("文本结束处理后需要做的一些事情")


# 子生成器
def writer():
    while True:
        try:
            t = yield
            print('>> %s' % t)
        except EmptyException:
            print('Error: Empty Line')


# 代理
def proxyWriter():
    before()
    result = yield from writer()
    return result


def app(text):
    w = proxyWriter()
    w.send(None)  # 激活writer
    for line in text:
        if line == '':
            w.throw(EmptyException)
        else:
            w.send(line)


# 启动app应用
app(('a', 'b', '', 'c'))

运行结果:

文本开始处理前需要做的一些事情
>> a
>> b
Error: Empty Line
>> c

总结:

**(1)yield from主要设计用来向子生成器委派操作任务,**但yield from可以向任意的可迭代对象委派操作;

**(2)委派生成器(group)相当于管道,**所以可以把任意数量的委派生成器连接在一起---一个委派生成器使用yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器。