30.4.8. 不要用send给生成器注入数据

yield表达式让我们能轻松地写出生成器函数,使得调用者可以每次只获取输出序列中的一项结果。但问题是,这种通道是单向的,即,无法让生成器在其一端接收数据流,同时在另一端给出计算结果。

假如能实现双向通信,那么生成器的使用面会更广。

例如,我们想用软件实现无线广播,用它来发送信号。为了编写这个程序,我们必须用一个函数来模拟正弦波,让它能够给出一系列按照正弦方式分布的点。

import math


def wave(amplitude, steps):
    step_size = 2 * math.pi / steps
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        output = amplitude * fraction
        yield output

有了这个wave函数,我们可以让它按照某个固定的振幅生成一系列供传输的值。

def transmit(output):
    if output is None:
        print(f'Output is None')
    else:
        print(f'Output: {output:>5.1f}')


def run(it):
    for output in it:
        transmit(output)

run(wave(3.0, 8))
"""
Output:   0.0
Output:   2.1
Output:   3.0
Output:   2.1
Output:   0.0
Output:  -2.1
Output:  -3.0
Output:  -2.1
"""

这样写可以生成基本的波形,但问题是,该函数在产生这些值的时候,只能按照刚开始给定的振幅来计算,而没办法使振幅在整个过程中根据某个因素发生变化。 现在,我们要让生成器在计算每个值的时候,都能考虑到振幅的变化,从而实现调幅。

Python的生成器支持send方法,这可以让生成器变为双向通道。send方法可以把参数发给生成器,让它为上一条yield表达式的求值结果,并将生成器推进到下一条yield表达式,然后把yield右边的值返回给send方法的调用者。 然而一般情况下,我们还是会通过内置的next函数来推进生成器,按照这种写法,上一条yield不表达式的求值结果总是None。

def my_generator():
    received = yield 1
    print(f'received = {received}')


it = my_generator()
output = next(it)
# 得到第一个生成器的输出
print(f'output = {output}')

try:
    next(it)
    # 推进生成器直到退出
except StopIteration:
    pass
else:
    assert False

"""
output = 1
received = None
"""

如果不通过for循环或内置的next函数推进生成器,而是改用send方法,那么调用方法时传入的参数就会成为上一条yield表达式的值,生成器拿到这个值后,会继续运行到下一条yield表达式那里。

可是,刚开始推进生成器的时候,它是从头执行的,而不是从某一条yield表达式那里继续的,所以,首次调用send方法时,只能传None,要是传入其他值,程序运行时就会抛出异常。

it = iter(my_generator())
output = it.send(None)
# 得到第一个生成器的输出
print(f'output={output}')
try:
    it.send('Hello!')
except StopIteration:
    pass

"""
output=1
received = Hello!
"""

我们可以利用这种机制让调用者把振幅发送过来,这样函数就能根据这个输入值调整生成的正弦波幅值了。首先修改wave函数的代码,让它把yield表达式的求值结果(也就是调用者通过send发过来的振幅)保存到amplitude变量里,这样就能根据该变量计算出下次应该生成的值。

import math

def wave_modulating(steps):
    step_size = 2 * math.pi / steps
    amplitude = yield               # 接收初始幅度
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        output = amplitude * fraction
        amplitude = yield output    # 接收下一个幅度

然后,要修改run函数调用wave_modulating函数的方式。它现在必须把每次所要使用的振幅发给wave_modulating生成器。首次必须发送None,因为此时生成器还没有遇到过yield表达式,它不需要知道上一条yield表达式的求值结果。

def transmit(output):
    if output is None:
        print(f'Output is None')
    else:
        print(f'Output: {output:>5.1f}')


def run_modulating(it):
    amplitudes = [
        None, 7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10
    ]
    for amplitude in amplitudes:
        output = it.send(amplitude)
        transmit(output)


run_modulating(wave_modulating(12))

"""
Output is None
Output:   0.0
Output:   3.5
Output:   6.1
Output:   2.0
Output:   1.7
Output:   1.0
Output:   0.0
Output:  -5.0
Output:  -8.7
Output: -10.0
Output:  -8.7
Output:  -5.0
"""

这样写在大方向上是对的,但问题在于:程序竟然输出了那么多None!这是为什么呢?因为每条yield from表达式其实都在遍历一个嵌套进去的生成器,所以每个嵌套生成器都必须分别执行它们各自的第一条yield语句(也就是什么值都不带的那条yield语句),只有执行过这条语句之后,这些生成器才能通过send方法所传来的值决定这条语句的求值结果,并把这个结果放在amplitude变量里以计算下一次应该输出的值。

所以complext_wave_modulating函数处理完前一个嵌套的生成器之后,会进入下一个嵌套的生成器,而这是就必须先把该生成器的第一条yield语句运行过去,这就导致后面两个嵌套生成器会各自从amlitudes列表里浪费掉一个值,并使得每个嵌套生成器所拿到的第一个结果必定是None,还会让最后那个嵌套生成器少执行两次。

也就是说,yield from语句和send方法结合使用效果不太让人满意。 最简单的一种写法,是把迭代器传给wave函数,让wave每次用到振幅的时候,通过Python内置的next函数推进这个迭代器并返回一个输入振幅。

import math


def wave_cascading(amplitude_it, steps):
    step_size = 2 * math.pi / steps
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        amplitude = next(amplitude_it)  # Get next input
        output = amplitude * fraction
        yield output

这样,我们只需要把同一个迭代器分别传给几条yield from语句里的wave_casading就行。迭代器是有状态的,所以下一个wave_cascading会从上一个使用完的地方,继续往下使用amplitude_it迭代器。

def complex_wave_cascading(amplitude_it):
    yield from wave_cascading(amplitude_it, 3)
    yield from wave_cascading(amplitude_it, 4)
    yield from wave_cascading(amplitude_it, 5)

要想触发这个组合的迭代器,只需要把振值放在列表汇总,并把针对列表制作的迭代器传给complex_wave_cascading就好。

def transmit(output):
    if output is None:
        print(f'Output is None')
    else:
        print(f'Output: {output:>5.1f}')


def run_cascading():
    amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
    it = complex_wave_cascading(iter(amplitudes))
    for amplitude in amplitudes:
        output = next(it)
        transmit(output)

这种写法最大的优点在于,迭代器可以来自任何地方,而且完全可以是动态的。此方案只有一个缺陷,就是必须假设入则输入的生成器绝对能保证线程安全。

要点:

send方法可以把数据注入生成器,让它成为上一条yield表达式的求值结果,生成器可以把这个结果赋给变量。

把send方法与yield from表达式搭配起来使用,可能导致奇怪的结果,例如会让程序在本该输出有效值的地方输出None。

通过迭代器向组合起来的生成器输入数据,要比采用send方法的那种方案好,所以尽量避免使用send

方法。