实际上,Python 3.3中新的"yield from"语法的主要用途是什么?

Pau*_*ine 349 python yield

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

  1. "收益率"有用的情况是什么?
  2. 什么是经典用例?
  3. 为什么它与微线程相比?

[更新]

现在我明白了我的困难的原因.我使用过发电机,但从未真正使用过协程(由PEP-342引入).尽管有一些相似之处,但生成器和协同程序基本上是两个不同的概念.理解协同程序(不仅仅是生成器)是理解新语法的关键.

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

感谢您的回答,但特别感谢agf和他与David Beazley演讲相关的评论.大卫摇滚.

Pra*_*ota 490

让我们先解决一件事.该解释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在Couroutines上的好奇课程是一个很好的开始.阅读幻灯片24-33以获得快速入门.

使用yield从生成器读取数据

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
Run Code Online (Sandbox Code Playgroud)

reader()我们可以yield from做到,而不是手动迭代.

def reader_wrapper(g):
    yield from g
Run Code Online (Sandbox Code Playgroud)

这很有效,我们删除了一行代码.也许意图有点清晰(或不是).但没有改变生活.

使用来自第1部分的yield将数据发送到生成器(协同程序)

现在让我们做一些更有趣的事情.让我们创建一个调用的协程writer,它接受发送给它的数据并写入socket,fd等.

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

现在的问题是,包装器函数应该如何处理向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
Run Code Online (Sandbox Code Playgroud)

包装器需要接受发送给它的数据(显然),并且还应该处理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
Run Code Online (Sandbox Code Playgroud)

或者,我们可以做到这一点.

def writer_wrapper(coro):
    yield from coro
Run Code Online (Sandbox Code Playgroud)

这样可以节省6行代码,使其更具可读性,而且只是有效.魔法!

将数据发送到生成器产生于 - 第2部分 - 异常处理

让它变得更复杂.如果我们的作家需要处理异常怎么办?让我们说writer句柄a SpamException,***如果它遇到一个它就打印出来.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)
Run Code Online (Sandbox Code Playgroud)

如果我们不改变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
Run Code Online (Sandbox Code Playgroud)

嗯,它不起作用,因为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
Run Code Online (Sandbox Code Playgroud)

这有效.

# Result
>>  0
>>  1
>>  2
***
>>  4
Run Code Online (Sandbox Code Playgroud)

但这也是如此!

def writer_wrapper(coro):
    yield from coro
Run Code Online (Sandbox Code Playgroud)

yield from透明地处理发送值或抛出的值到副发电机.

尽管如此,这仍然不能涵盖所有角落的情况.如果外部发电机关闭会发生什么?如果子生成器返回一个值(是的,在Python 3.3+中,生成器可以返回值),那么返回值应该如何传播呢?yield from透明地处理所有角落的情况下确实是令人印象深刻.yield from只是神奇地工作并处理所有这些案件.

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

总之,最好将其yield from视为transparent two way channel调用者和子生成器之间的关系.

参考文献:

  1. PEP 380 - 委托给子发电机的语法(尤因)[v3.3,2009-02-13]
  2. PEP 342 - 通过增强型发电机的协同程序(GvR,Eby)[v2.5,2005-05-10]

  • @PraveenGollakota,在你的问题的第二部分,**使用yield - 第1部分**将数据发送到生成器(协同程序),如果你有多个协程转发收到的项目怎么办?就像广播公司或订阅者场景一样,您在示例中为包装器提供了多个协同程序,并且应该将项目发送给它们的全部或部分? (3认同)
  • 在阅读"**甚至没有开始正义**"时,我知道我已经得到了正确的答案.谢谢你的好解释! (3认同)
  • 在 `while True:` 循环中执行 ` except StopIteration: pass` 并不是 `yield from coro` 的准确表示 - 这不是一个无限循环,并且在 `coro` 耗尽后(即引发 StopIteration), `writer_wrapper` 将执行下一条语句。在最后一条语句之后,它会像任何耗尽的生成器一样自动引发“StopIteration”... (2认同)
  • 令我惊讶的是,他们没有使用“yield as”而不是“yield from”。语义变得更加清晰:在该语句的持续时间内,基本上表现为*作为*被调用的协程,就好像用户直接调用它一样。(我花了这个答案才意识到,正是因为“yield from”所暗示的含义与这个答案所清楚解释的内容并没有直观的联系。) (2认同)
  • 生成器包装器的主要用途是什么? (2认同)
  • @JamesLin:*仅*执行“yield from g”的包装器没有用。然而,包装器可以在“yield from”之前或之后执行其他操作,例如打开文件、打开和关闭套接字或任何其他操作。或者它可以顺序执行多个“yield from”操作。想象一个连接生成器的生成器:“def gcat(generators): for g in Generators: yield from g”。 (2认同)

Nik*_* B. 80

"收益率"有用的情况是什么?

你有这样一个循环的每种情况:

for x in subgenerator:
  yield x
Run Code Online (Sandbox Code Playgroud)

正如PEP所描述的那样,这是一个相当天真的尝试使用子发电机,它缺少几个方面,尤其是PEP 342引入的.throw()/ .send()/ .close()机制的正确处理.要做到这一点,需要相当复杂的代码.

什么是经典用例?

考虑您要从递归数据结构中提取信息.假设我们想要获取树中的所有叶节点:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)
Run Code Online (Sandbox Code Playgroud)

更重要的是,在此之前yield from,没有简单的重构生成器代码的方法.假设您有一个(无意义的)生成器,如下所示:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)
Run Code Online (Sandbox Code Playgroud)

现在,您决定将这些循环分解为单独的生成器.没有yield from,这是丑陋的,直到你会想三次是否真的想要这样做.有了yield from,看看真的很好:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)
Run Code Online (Sandbox Code Playgroud)

为什么它与微线程相比?

我认为PEP中的这一部分所讨论的是每个生成器都有自己独立的执行上下文.与使用yield__next__()分别在生成器 - 迭代器和调用者之间切换执行的事实一起,这类似于线程,其中操作系统不时地切换执行线程,以及执行上下文(堆栈,寄存器, ...).

这种效果也是可比的:生成器迭代器和调用者同时在执行状态中进行,它们的执行是交错的.例如,如果生成器进行某种计算并且调用者打印出结果,您将在结果可用时立即看到结果.这是一种并发形式.

尽管如此,这个类比并不是特定的yield from- 它是Python中生成器的一般属性.


Ben*_*son 29

无论你从发电机内调用发电机,都需要一个"泵"来重新定义yield值: for v in inner_generator: yield v.正如人民党指出的那样,大多数人都忽略了这种复杂性.非本地流量控制就像throw()是PEP中给出的一个例子.yield from inner_generator无论您何时编写显式for循环,都会使用新语法.不过,它不仅仅是语法糖:它处理for循环忽略的所有极端情况."含糖"鼓励人们使用它,从而获得正确的行为.

讨论主题中的这条消息谈到了这些复杂性:

由于PEP 342引入了额外的生成器功能,情况不再如此:如Greg的PEP中所述,简单的迭代不能正确支持send()和throw().当你打破它们时,支持send()和throw()所需的体操实际上并不复杂,但它们也不是微不足道的.

我不能说与微线程进行比较,除了观察发电机是一种类型的并行性.您可以将挂起的生成器视为将值发送yield到使用者线程的线程.实际的实现可能与此类似(并且实际的实现显然是Python开发人员非常感兴趣的),但这与用户无关.

yield from语法在线程方面不会为语言添加任何其他功能,只是使正确使用现有功能变得更加容易.或者更确切地说,它使得专家编写的复杂内部生成器的新手消费者更容易通过该生成器而不会破坏其任何复杂特征.


osp*_*der 21

一个简短的例子将帮助您理解其中一个yield from用例:从另一个生成器获取值

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))
Run Code Online (Sandbox Code Playgroud)

  • 只是想建议如果不转换为列表,最后的打印看起来会更好一些 - `print(*flatten([1, [2], [3, [4]]]))` (4认同)

kat*_*330 13

yield将产生单一值到集合中。

yield from将把集合变成集合并使其扁平化。

检查这个例子:

def yieldOnly():
    yield "A"
    yield "B"
    yield "C"

def yieldFrom():
    for i in [1, 2, 3]:
        yield from yieldOnly()

test = yieldFrom()
for i in test:
    print(i)
Run Code Online (Sandbox Code Playgroud)

在控制台中您将看到:

A
B
C
A
B
C
A
B
C
Run Code Online (Sandbox Code Playgroud)


Yeo*_*Yeo 9

在应用的使用为异步IO协程yield from也有类似的行为作为await协程功能。两者都用于暂停协程的执行。

对于 Asyncio,如果不需要支持较旧的 Python 版本(即 >3.5),async def/await是定义协程的推荐语法。因此yield from在协程中不再需要。

但总的来说,在 asyncio 之外,yield from <sub-generator>在迭代子生成器方面还有一些其他用途,如前面的答案中所述。


Joc*_*zel 7

yield from 基本上以有效的方式链接迭代器:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it
Run Code Online (Sandbox Code Playgroud)

如您所见,它删除了一个纯 Python 循环。这几乎就是它所做的一切,但是链接迭代器是 Python 中非常常见的模式。

线程基本上是一个特性,它允许你在完全随机的点跳出函数并跳回到另一个函数的状态。线程主管经常这样做,因此程序似乎同时运行所有这些功能。问题是点是随机的,所以你需要使用锁定来防止主管在有问题的点停止功能。

从这个意义上说,生成器与线程非常相似:它们允许您指定yield可以跳入和跳出的特定点(无论何时)。以这种方式使用时,生成器称为协程。

阅读这篇关于 Python 中协程的优秀教程以获取更多详细信息

  • 这个答案具有误导性,因为它忽略了“yield from”的显着特征,如上所述:send() 和 throw() 支持。 (11认同)
  • 您是否对本·杰克逊的上述回答提出异议?我对您的回答的解读是,它本质上是遵循您提供的代码转换的语法糖。本杰克逊的回答明确驳斥了这一说法。 (5认同)
  • @Justin W:我猜你之前读到的任何内容实际上都具有误导性,因为你没有明白 `throw()/send()/close()` 是 `yield` 特性,而 `yield from` 显然必须实现这些特性正确,因为它应该简化代码。这些琐碎的事情与使用无关。 (2认同)