Python中yield from的用法
上一篇中谈到了迭代器、生成器以及yield的相关内容,而Python 3.3中,引入了在PEP 380 – Syntax for Delegating to a Subgenerator中提出的yield from关键字,大幅简化了Python程序员在使用协程的时候的编程过程。
首先需要声明的是,yield from g并不完全等于for v in g: yield v。而是应该将yield from看成为调用者(caller)和子生成器(sub-generator)之间提供了一种透明地双向通道。这包括了从子生成器中获取数据并向子生成器发送数据。
使用yield from从生成器中获得数据
考虑以下代码:
|
def reader():
"""A generator that fakes a read from a file, socket, etc."""
for i in range(4):
yield '<< %s' % i
def reader_wrapper(g):
# Manually iterate over data produced by reader
for v in g:
yield v
wrap = reader_wrapper(reader())
for i in wrap:
print(i)
# Result
<< 0
<< 1
<< 2
<< 3
|
我们其实可以使用yield from来代替亲自迭代reader():
|
def reader_wrapper(g):
yield from g
|
这可以很好的工作而且减少了一行代码,而且可能使得我们的意图更加明确。
使用yield from向生成器发送数据
现在让我们做些更有趣的。首先创建一个名叫writer的协程,它可以接收发送给它的数据并写给套接字、文件描述符等等:
|
def writer():
"""A coroutine that writes data *sent* to it to fd, socket, etc."""
while True:
w = (yield)
print('>> ', w)
|
现在的问题是,包装函数wrapper如何处理将数据发送给writer,使得发送给包装函数的数据能够透明地发送给writer()?
|
def writer_wrapper(coro):
# TBD
pass
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in range(4):
wrap.send(i)
# Expected result
>> 0
>> 1
>> 2
>> 3
|
包装函数需要接受发送给它的数据(显而易见地)而且应该在循环结束的时候处理StopIteration异常。很明显只是完成for x in coro: yield x的话不能胜任这项工作。下面是一个能够工作的版本:
|
def writer_wrapper(coro):
coro.send(None) # prime the coro
while True:
try:
x = (yield) # Capture the value that's sent
coro.send(x) # and pass it to the writer
except StopIteration:
pass
|
或者,我们可以这样做:
|
def writer_wrapper(coro):
yield from coro
|
这节省了6行代码,而且使得代码更加清晰易读,最关键的是,它可行!
使用yield from向生成器发送数据——异常处理
让我们使这个例子更复杂点,假设我们的writer需要处理异常呢?比如writer捕获SpamException异常并且在遇到这个的时候打印***。
|
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
|
如果我们使用原始版本的writer_wrapper,会怎样?
|
# writer_wrapper same as above
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
if i == 'spam':
wrap.throw(SpamException)
else:
wrap.send(i)
# Expected Result
>> 0
>> 1
>> 2
***
>> 4
# Actual Result
>> 0
>> 1
>> 2
Traceback (most recent call last):
... redacted ...
File ... in writer_wrapper
x = (yield)
__main__.SpamException
|
不能正常工作的原因是因为x = (yield)抛出了这个异常所以导致了程序崩溃。要使得其正常工作的话,我们需要亲自捕获异常并将它传递给子生成器(writer):
|
def writer_wrapper(coro):
"""Works. Manually catches exceptions and throws them"""
coro.send(None) # prime the coro
while True:
try:
try:
x = (yield)
except Exception as e: # This catches the SpamException
coro.throw(e)
else:
coro.send(x)
except StopIteration:
pass
...
# Result
>> 0
>> 1
>> 2
***
>> 4
|
这可以正常工作,但是假设我们这样呢:
|
def writer_wrapper(coro):
yield from coro
|
yield from语句透明地将数据或者异常发送给子生成器。
以上仍然没有覆盖所有的特殊情况。如果外部生成器关闭了会怎样?子生成器返回了一个值会怎样(Python 3里生成器可以返回值)?返回值会怎样被处理?而yield from很好的处理了以上所有情况。
总结
yield from是调用者和子生成器之间的一个双向透明通道。更多关于yield from的内容可以阅读PEP 380 – Syntax for Delegating to a Subgenerator。

浙公网安备 33010602011771号