python 并发专题(四):yield以及 yield from

一、yield

          python中yield的用法很像return,都是提供一个返回值,但是yield和return的最大区别在于,return一旦返回,则代码段执行结束,但是yield在返回值以后,会交出CUP的使用权,代码段并没有直接结束,而是在此处中断。

         当调用send()或者next()方法之后,yield可以从之前中断的地方继续执行。

        在一个函数中,使用yield关键字,则当前的函数会变成生成器。

实例解析:

生成一个斐波那契数列。

def fib(n):
    index = 0
    a = 0
    b = 1

    while index < n:
        yield b
        a,b = b, a+b
        index += 1

1.生成器对象

fib = fib(100)
print(fib)

 

 

 打印出来的结果是一个生成器对象,并没有直接把我们想要的值打印出来。

2.next()方法

fib = fib(100)
print(next(fib))
print(next(fib))
print(next(fib))
print(next(fib))

 

 

 它的执行顺序是这样的,每次yield返回之后,程序将会中断,当出现next(fib)之后,程序将会从之前中断的地方继续执行。 python新版本中,不再提供fib.next()方法。

3 send()方法

使用send()方法允许我们向生成器中传值。

import time

def fib(n):
    index = 0
    a = 0
    b = 1

    while index < n:
        sleep = yield b
        print('等待%s秒' %sleep)
        time.sleep(sleep)
        a,b = b, a+b
        index += 1

fib = fib(20)
print(fib.send(None))   # 效果等同于print(next(fib))
print(fib.send(2))
print(fib.send(2))
print(fib.send(2))
print(fib.send(2))

 

 

 执行顺序如下:
首先,创建生成器对象
调用fib.send(None)方法,此处作用与next(fib)相同,程序返回当前b的值1, 程序中断。
调用fib.send(2)方法,程序被唤醒,将2传递给yield之前的变量sleep,程序继续运行,直到遇到yield将新的b返回,程序再次中断。
如此继续下去,直到程序结束。

二、yield from

例子一:生成器嵌套,简化代码

前面的都是单一层次的生成器,并没有嵌套,如果是多个生成器嵌套会怎么样呢,下面是一个例子。

def fun_inner():
    i = 0
    while True:
        i = yield i

def fun_outer():
    a = 0
    b = 1
    inner = fun_inner()
    inner.send(None)
    while True:
        a = inner.send(b)
        b = yield a

if __name__ == '__main__':
    outer = fun_outer()
    outer.send(None)
    for i in range(5):
        print(outer.send(i))

 

 

 在两层嵌套的情况下,值的传递方式是,先把值传递给外层生成器,外层生成器再将值传递给外层生成器,内层生成器在将值反向传递给外层生成器,最终yield出结果。如果嵌套的层次更多,传递将会越麻烦。

下面是yield from的实现方式:

def fun_inner():
    i = 0
    while True:
        i = yield i

def fun_outer():
    yield from fun_inner()

if __name__ == '__main__':
    outer = fun_outer()
    outer.send(None)
    for i in range(5):
        print(outer.send(i))

 

 

 效果是一样的,但是明显的代码量减少了,嵌套传值的时候,并不需要我们手动实现。

例子二:yield from 可用于简化 for 循环中的 yield 表达式。

>>> def gen():
...     for c in 'AB':
...         yield c
...     for i in range(1, 3):
...         yield i
...
>>> list(gen())
['A', 'B', 1, 2]
# 变为
>>> def gen():
...     yield from 'AB'
...     yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]

例子三:使用 yield from 链接可迭代的对象

>>> def chain(*iterables):
...     for it in iterables:
...         yield from it
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象

             yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责

例子四:

委派生成器
        包含 yield from <iterable> 表达式的生成器函数。
子生成器

        从 yield from 表达式中 <iterable> 部分获取的生成器。这就是PEP 380 的标题(“Syntax for Delegating to a Subgenerator”)中所说的“子生成器”(subgenerator)。
调用方

        PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子生成器)区分开。

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

from collections import namedtuple

Result = namedtuple('Result', 'count average')

# 子生成器
def averager():  #这里作为子生成器使用
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield  #main 函数中的客户代码发送的各个值绑定到这里的 term 变量上
        if term is None:  #至关重要的终止条件。如果不这么做,使用 yield from 调用这个协程的生成器会永远阻塞。
break
        total += term
        count += 1
        average = total/count
    return Result(count, average)  #返回的 Result 会成为 grouper 函数中 yield from 表达式的值。

# 委派生成器
def grouper(results, key):  #grouper 是委派生成器
    while True:  #这个循环每次迭代时会新建一个 averager 实例;每个实例都是作为协程使用的生成器对象。
        results[key] = yield from averager()  #grouper 发送的每个值都会经由 yield from 处理,通过管道传给averager 实例。
#grouper 会在 yield from 表达式处暂停,等待averager 实例处理客户端发来的值。
#averager 实例运行完毕后,返回的值绑定到 results[key] 上。
#while 循环会不断创建 averager 实例,处理更多的值。
# 客户端代码,即调用方 def main(data): #main 函数是客户端代码,用 PEP 380 定义的术语来说,是“调用方”。这是驱动一切的函数。 results = {} for key, values in data.items(): group = grouper(results, key) #group 是调用 grouper 函数得到的生成器对象,传给 grouper 函数的第一个参数是 results,用于收集结果;
#第二个参数是某个键。group 作为协程使用 next(group) #预激 group 协程。
for value in values: group.send(value) #把各个 value 传给 grouper。传入的值最终到达 averager 函数中term = yield 那一行;
#grouper 永远不知道传入的值是什么。
        group.send(None)  # 重要!  ⓬把 None 传入 grouper,导致当前的 averager 实例终止,也让grouper 继续运行,
#再创建一个 averager 实例,处理下一组值。
    # print(results)  # 如果要调试,去掉注释
    report(results)

# 输出报告
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(
              result.count, group, result.average, unit))

data = {
    'girls;kg':
        [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m':
        [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg':
        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m':
        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}

if __name__ == '__main__':
    main(data)

运作方式说明

1.外层 for 循环每次迭代会新建一个 grouper 实例,赋值给 group变量;group 是委派生成器。
2.调用 next(group),预激委派生成器 grouper,此时进入 whileTrue 循环,调用子生成器 averager 后,在 yield from 表达式处暂停。
3.内层 for 循环调用 group.send(value),直接把值传给子生成器averager。同时,当前的 grouper 实例(group)在 yield from 表达式处暂停。
4.内层循环结束后,group 实例依旧在 yield from 表达式处暂停,因此,grouper 函数定义体中为 results[key] 赋值的语句还没有执行。
5.如果外层 for 循环的末尾没有 group.send(None),那么averager 子生成器永远不会终止,委派生成器 group 永远不会再次激活,因此永远不会为 results[key] 赋值。
6.外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到group 变量上。前一个 grouper 实例(以及它创建的尚未终止的averager 子生成器实例)被垃圾回收程序回收。
综述

       (把迭代器当作生成器使用,相当于把子生成器的定义体内联在yield from 表达式中。此外,子生成器可以执行 return 语句,返回一个值,而返回的值会成为 yield from 表达式的值)

例子四 阐明了下述四点:

1.子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。

2.使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。

3.生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。

4.yield from 表达式的值是子生成器终止时传给 StopIteration异常的第一个参数。

yield from 结构的另外两个特性与异常和终止有关。

1.传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出StopIteration 异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。

2.如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用 close() 方法,如果它有的话。如果调用 close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit 异常。

 协程:通过客户调用 .send(...) 方法发送数据或使用 yield from 结构驱动的生成器函数。


posted @ 2020-04-17 16:46  秋华  阅读(750)  评论(0编辑  收藏  举报