小编典典

在实践中,Python 3.3 中“yield from”语法的主要用途是什么?

all

我很难将我的大脑包裹在PEP 380周围。

  1. 什么情况下yield from有用?
  2. 什么是经典用例?
  3. 为什么与微线程相比?

到目前为止,我使用了生成器,但从未真正使用过协程(由PEP-342引入)。尽管有一些相似之处,但生成器和协程基本上是两个不同的概念。理解协程(不仅仅是生成器)是理解新语法的关键。

恕我直言 ,协程是最晦涩的 Python 功能 ,大多数书籍使它看起来无用且无趣。


阅读 188

收藏
2022-03-09

共1个答案

小编典典

让我们先解决一件事。yield from g等同于的解释for v in g: yield v 甚至没有开始公正 地对待yield from所有事情。因为,让我们面对现实吧,如果yield from只是扩展for循环,那么它就不能保证添加yield from到语言中并排除一大堆新功能在 Python 2.x 中实现。

yield from它的作用是在 调用者和子生成者之间建立透明的双向连接

  • 从某种意义上说,连接是“透明的”,它也将正确传播所有内容,而不仅仅是正在生成的元素(例如传播异常)。

  • 连接是“双向的”,因为数据既可以 生成器发送也可以发送 生成器。

如果我们在谈论 TCP,yield from g可能意味着“现在暂时断开我客户端的套接字并将其重新连接到另一个服务器套接字”。

顺便说一句,如果您不确定 将数据发送到生成器 意味着什么,您需要放弃所有内容并首先阅读 协程 ——它们非常有用(将它们与 子例程
对比),但不幸的是在 Python 中鲜为人知。Dave Beazley 的 Coroutines Curious
Course
是一个很好的开始。阅读幻灯片
24-33
以获得快速入门。

使用 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

而不是手动迭代reader(),我们可以就yield from这样。

def reader_wrapper(g):
    yield from g

那行得通,我们消除了一行代码。并且可能意图更清晰(或不清晰)。但生活没有任何改变。

使用 yield from 将数据发送到生成器(协程) - 第 1 部分

现在让我们做一些更有趣的事情。让我们创建一个名为的协程writer,它接受发送给它的数据并写入套接字、fd 等。

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

现在的问题是,包装器函数应该如何处理向写入器发送数据,以便发送到包装器的任何数据都 透明地 发送到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 循环耗尽时处理。显然只是做是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 行代码,使其更具可读性,并且可以正常工作。魔法!

从 - 第 2 部分 - 异常处理向生成器发送数据

让我们把它变得更复杂。如果我们的编写器需要处理异常怎么办?假设writer句柄 a
,如果遇到一个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.3+
中,生成器可以返回值),返回值应该如何传播?透明地处理所有yield from角落案例确实令人印象深刻yield from只是神奇地工作并处理所有这些情况。

我个人认为yield from这是一个糟糕的关键字选择,因为它不会使 双向
性质明显。还提出了其他关键字(例如delegate但被拒绝,因为向语言中添加新关键字比组合现有关键字要困难得多。

总之,最好将其yield from视为 transparent two way channel 调用者和子生成者之间的一个。

参考:

  1. PEP 380 - 委派给子生成器的语法 (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - 通过增强生成器的协程(GvR,Eby)[v2.5,2005-05-10]
2022-03-09