小能豆

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

py

我很难理解PEP 380。

在什么情况下yield from有用?
经典的用例是什么?
为什么要与微线程进行比较?
到目前为止,我使用过生成器,但从未真正使用过协程(由PEP-342引入)。尽管有一些相似之处,但生成器和协程基本上是两个不同的概念。理解协程(而不仅仅是生成器)是理解新语法的关键。

我认为协程是 Python 中最晦涩的功能,大多数书籍都使它看起来无用且无趣。


阅读 19

收藏
2024-09-20

共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 的《协程奇妙课程》是一个很好的开始。阅读幻灯片 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向生成器(协程)发送数据 - 第一部分

现在让我们做一些更有趣的事情。让我们创建一个名为的协程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

包装器需要接受发送给它的数据(显然),还应该处理StopIterationfor 循环耗尽的情况。显然,只这样做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 部分 - 异常处理

让我们让它更复杂一些。如果我们的编写器需要处理异常怎么办?假设处理writeraSpamException并且***如果遇到异常,它会打印出来。

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]
2024-09-20