在实践中,python3.3中新的”yield-from”语法的主要用途是什么?

In practice, what are the main uses for the new “yield from” syntax in Python 3.3?

我很难在PEP 380上下功夫。

  • 什么情况下"屈服"是有用的?
  • 什么是经典用例?
  • 为什么与微线程相比?
  • [更新]

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

    imho coroutines是最晦涩的python特性,大多数书籍都让它看起来毫无用处和无趣。

    感谢您的回答,但特别感谢AGF和他与DavidBeazley演讲相关的评论。戴维岩。


    我们先把一件事解决。关于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中的知名度较低。戴夫·比兹利关于古鲁特的好奇课程是一个很好的开始。阅读幻灯片24-33快速入门。

    使用产量从发电机读取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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即可。

    1
    2
    def reader_wrapper(g):
        yield from g

    这是可行的,我们去掉了一行代码。可能意图更清楚(或不清楚)。但生活没有改变。

    使用第1部分的产量将数据发送到发电机(协程)

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

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

    现在的问题是,包装器函数应该如何处理向编写器发送数据,以便将发送到包装器的任何数据透明地发送到writer()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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

    包装器需要接受发送给它的数据(显然),并且在for循环耗尽时还应该处理StopIteration。显然,仅仅做一件事情是行不通的。这是一个有效的版本。

    1
    2
    3
    4
    5
    6
    7
    8
    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

    或者,我们可以这样做。

    1
    2
    def writer_wrapper(coro):
        yield from coro

    这就节省了6行代码,使其更具可读性,而且还可以正常工作。魔术!

    从第2部分异常处理向发电机发送数据

    让我们把事情弄复杂一点。如果我们的作者需要处理异常呢?假设writer处理一个SpamException,如果遇到一个***就会打印出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class SpamException(Exception):
        pass

    def writer():
        while True:
            try:
                w = (yield)
            except SpamException:
                print('***')
            else:
                print('>> ', w)

    如果我们不改变writer_wrapper呢?它起作用了吗?让我们试试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # 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)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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

    这是可行的。

    1
    2
    3
    4
    5
    6
    # Result
    >>  0
    >>  1
    >>  2
    ***
    >>  4

    但这也是!

    1
    2
    def writer_wrapper(coro):
        yield from coro

    yield from透明地处理发送值或向子生成器中抛出值。

    不过,这还没有涵盖所有的角落案件。如果外部发电机关闭会发生什么?当子生成器返回一个值(是的,在Python3.3+中,生成器可以返回值)时,该如何传播返回值?yield from透明地处理所有的角箱,真是令人印象深刻。yield from只是神奇地工作和处理所有这些案件。

    我个人认为yield from是一个糟糕的关键字选择,因为它没有使双向性变得明显。还有其他关键词被提出(如delegate),但由于向语言中添加新的关键词比组合现有关键词困难得多而被拒绝。

    总之,最好把yield from看作是主叫方和子生成器之间的transparent two way channel

    参考文献:

  • PEP 380-委托给子生成器(Ewin)的语法[v3.32009-02-13]
  • PEP 342通过增强型发电机(GVR,EBY)的协程[2005-05-10年2月5日]

  • What are the situations where"yield from" is useful?

    每种情况下都会出现这样的循环:

    1
    2
    for x in subgenerator:
      yield x

    正如PEP所描述的,这是一个相当幼稚的使用子生成器的尝试,它缺少几个方面,特别是PEP 342引入的.throw()/.send()/.close()机制的正确处理。要正确执行此操作,需要相当复杂的代码。

    What is the classic use case?

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

    1
    2
    3
    4
    5
    def traverse_tree(node):
      if not node.children:
        yield node
      for child in node.children:
        yield from traverse_tree(child)

    更重要的是,在yield from之前,没有简单的方法来重构生成器代码。假设您有这样一个(无意识的)生成器:

    1
    2
    3
    4
    5
    6
    7
    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)

    现在您决定将这些循环分解成单独的生成器。如果没有yield from,这是很难看的,直到你会三思而后行。使用yield from,实际上很高兴看到:

    1
    2
    3
    4
    5
    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)

    Why is it compared to micro-threads?

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

    这样做的效果也相当:生成器迭代器和调用程序在执行状态下同时进行,它们的执行是交错的。例如,如果生成器进行某种计算,并且调用程序打印出结果,那么一旦结果可用,您就会看到结果。这是一种并发形式。

    不过,这种类比并不是yield from特有的,而是python中生成器的一般属性。


    无论从发电机内部调用发电机,都需要一个"泵"来重新输入cx1(6)的值:for v in inner_generator: yield v。正如政治公众人物指出的那样,这其中存在着许多人忽视的微妙的复杂性。throw()等非局部流量控制就是PEP中给出的一个例子。新语法yield from inner_generator在您以前编写显式for循环的任何地方都会使用。不过,它不仅仅是语法上的糖分:它处理所有被for循环忽略的角落案例。"甜言蜜语"鼓励人们使用它,从而获得正确的行为。

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

    With the additional generator features introduced by PEP 342, that is no
    longer the case: as described in Greg's PEP, simple iteration doesn't
    support send() and throw() correctly. The gymnastics needed to support
    send() and throw() actually aren't that complex when you break them
    down, but they aren't trivial either.

    除了观察到生成器是一种平行性之外,我不能用微线程来进行比较。您可以将挂起的生成器视为一个线程,它通过yield向使用者线程发送值。实际的实现可能与此不同(而且实际的实现显然对Python开发人员非常感兴趣),但这与用户无关。

    新的yield from语法没有在线程方面为语言添加任何额外的功能,它只是使正确使用现有功能变得更容易。或者更准确地说,它使专家编写的复杂内部生成器的初学者更容易通过该生成器而不破坏其任何复杂特性。


    会帮助你了解在短期example of S是一yield from使用get from another发电机房屋价值:P></

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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]]])))

    在异步协同应用for the usage yield from我有相似行为,在AS中await协同功能。both of which is used to the of弦支穹顶结构的进程执行。P></

    • yield fromis used by the发生器基于协同。P></

    • awaitis used for async def协同。(因为Python 3.5 +)P></

    我不asyncio for、if need to an基础支持Python版本(即>3.5),async def/ awaitis the syntax to define a进程建议。thus yield fromis needed不周的进程中。P></

    但在外面asyncio通用yield from has some of,the other仍然在使用迭代子发生器在回答上述earlier as the。P></


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 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

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

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

    从这个意义上讲,生成器与线程非常相似:它们允许您指定特定的点(每当它们yield时),您可以在其中跳入和跳出。当用这种方式时,发电机被称为协程。

    阅读关于Python中协程的优秀教程了解更多详细信息