Contents
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
方法。