第 34 条 不要用 send 给生成器注入数据
yield 表达式让我们能够轻松地写出生成器函数,使得调用者可以每次只获取输出序列中的一项结果(参见第 30 条)。但问题是,这种通道是单向的,也就是说,无法让生成器其一端接收数据,同时在另一端给出计算结果。假如能实现双向通信,那么生成器的适应面回更广。
例如,我们想用软件实现无线电广播,用它来发信号。为了编写这个程序,我们必须用一个函数来模拟正弦波,让它能够给出一系列按照正弦方式分布的点。
import math
def wave(amplitude, steps):
step_size = 2 * math.pi / steps
for step in range(step_size):
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 = iter(my_generator())
output = next(it)
print(f'output = {output}')
try:
next(it)
except StopIteration:
pass
>>>
output = 1
received = None
如果不通过 for 循环或内置的 next 函数推进生成器,而是改用 send 方法,那么调用方法时传入的参数就会成为上一条 yield 表达式的值,生成器拿到这个值后,会继续运行到下一条 yield 表达式那里。可是,刚开始推进生成器的时候,它是从头执行的,而不是从某一条 yield 表达式那里继续的,所以,首次调用 send 方法时,只能传 None,要是传入其他值,程序运行时就会抛出异常。
it = iter(my_generator())
output = it.send(None) # Get first generator
print(f'output = {output}')
try:
it.send('hello!') # Send value into the generator
except StopIteration:
pass
>>>
output = 1
received = hello!
我们可以利用这种机制让调用者把振幅发送过来,这样函数就能根据这个输入值调整生成的正弦波幅值了。首先修改 wave 函数的代码,让它把 yield 表达式的求值结果(也就是调用者通过 send 发过来的振幅)保存到 amplitude 变量里,这样就能根据该变量计算下次应该生成的值。
def wave_modulating(steps):
step_size = 2 * math.pi / steps
amplitude = yield # Received initial amplitude
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
output = amplitude * fraction
amplitude = yield output # Receive next amplitude
然后,要修改 run 函数调用 wave_modulating 函数的方式。它现在必须把每次所要使用的振幅发给 wave_modulating 生成器。首次必须发送 None,因为此时生成器还没有遇到过 yield 表达式,它不需要知道上一条 yield 表达式的求值结果。
def run_modulating(it):
amplitudes = [
None, 7, 7, 7, 2, 2, 2, 2, 10, 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,因为此时 wave_modulating 函数并不知道 amplitude 是多少,它只有在运行第一条 yield 表达式后,才能把调用者通过 send 发来的值保存到这个变量里。
这种写法有个缺点,就是它很难让初次阅读这段代码的人立刻理解它的意思。把 yield 放在赋值语句的右侧,本身就不太直观,因为我们必须先了解生成器这个高级特性,才能明白 yield 与 send 是如何配合的。
现在假设程序的需求变得更复杂了。这次要生成的载波不是简单的正弦波,而是由多个信号序列构成的复合波形。要实现这个需求,可以连用几条 yield from 语句,把多个生成器接起来(参见 第 33 条)。下面先来验证一下,这种写法能否处理振幅固定的情况。
def complex_wave():
yield from wave(7.0, 3)
yield from wave(2.0, 4)
yield from wave(10.0, 5)
run(complex_wave())
>>>
Output: 0.0
Output: 6.1
Output: -6.1
Output: 0.0
Output: 2.0
Output: 0.0
Output: -2.0
Output: 0.0
Output: 9.5
Output: 5.9
Output: -5.9
Output: -9.5
可以看到,利用 yield from 表达式确实可以处理比较简单的情况。那既然这样,我们再试试看,它能否处理那种采用 send 方法写成的函数。下面就用这种写法,接连多次调用 wave_modulating 生成器。
def complex_wave_modulating():
yield from wave_modulating(3)
yield from wave_modulating(4)
yield from wave_modulating(5)
run_modulating(complex_wave_modulating())
>>>
Output is None
Output: 0.0
Output: 6.1
Output: -6.1
Output is None
Output: 0.0
Output: 2.0
Output: 0.0
Output: -10.0
Output is None
Output: 0.0
Output: 9.5
Output: 5.9
Output: -5.9
这样写大方向是对的,但问题在于:程序竟然输出了那么多 None!这是为什么呢?因为每条 yield from 表达式其实都遍历一个嵌套进去的生成器,所以每个嵌套器都必须分别执行它们各自的第一条 yield 语句,只有执行过这条语句后,这些生成器才能通过 send 方法所传来的值决定这条语句的求值结果,并把这个结果放在 amplitude 变量里以计算下一次输出的值。所以,complex_wave_modulating 函数处理完前一个嵌套的生成器之后,会进入下一个嵌套的生成器,而这时就必须先把生成器的第一条 yield 语句运行过去,这就导致后面两个嵌套生成器会各自从 amplitudes 列表里浪费一个值,并使得每个嵌套生成器所拿到的第一个结果必定是 None,还会让最后那个嵌套生成器少执行两次。
这意味着,虽然 yield from 语句与 send 方法单独用着都没有问题,然而结合使用的效果却不太让人满意。当然也可以在 run_modulating 函数里添加一些代码,使得计算波形的步调与 amplitudes 列表提供的振幅值步调一致,但这只能使程序越写越复杂,因为利用 send 方法写成的 wave_modulating 函数本身已经不太直观了,而我们又把这个生成器函数通过 yield from 语句套到了 complex_wave_modulating 里面,这理解起来就更加困难了。所以,建议抛开 send 方法,改用更简单的方法来做。
最简单的方法,是把迭代器传给 wave 函数,让 wave 每次用到振幅的时候,通过 Python 内置的 next 函数推进这个迭代器并返回一个输入振幅。于是,这就促使多个生成器之间,产生连锁反应(其他类似案例参见 第32条)。
def wave_cascading(amplitude_it, steps):
step_size = 2 * math.pi / steps
for step in range(steps):
radians = step * step * step_size
fraction = math.sin(radians)
amplitude = next(amplitude_it)
output = amplitude * fraction
yield output
这样,我们只需要把同一个迭代器分别传给这几条 yield from 语句里的 wave_cascading 就行。迭代器是由状态的(参见 第 31 条),所以下一个 wave_cascading 会从上一个 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 run_cascading():
amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10, 10]
it = complex_wave_cascading(iter(amplitudes))
for amplitude in amplitudes:
output = next(it)
transmit(output)
run_cascading()
这种写法最大的优点在于,迭代器可以来自任何地方,而且完全可以是动态的。此方案只有一个缺陷,就是必须假设负责输出的生成器能保证线程安全,但有时其实保证不了这一点。如果代码要跨越边界,那么用 async 函数实现可能更好(参见 第 62 条)。