在实践中,Python 3.3中新的“yield from”语法的主要用途是什么?
我正在艰难地围绕PEP 380环绕我的大脑。
- “收益来源”有用的情况是什么?
- 什么是经典用例?
- 为什么它与微细线相比?
[更新]
现在我明白我的困难的原因。 我用过发电机,但从来没有真正使用过协程(由PEP-342引入)。 尽pipe有一些相似之处,发生器和协程基本上是两个不同的概念。 了解协程(不仅是生成器)是了解新语法的关键。
恕我直言协程是最晦涩的Pythonfunction ,大多数书籍使它看起来无用和无趣。
感谢大家的好评 ,但特别感谢agf和他的评论与David Beazley的介绍 。 大卫岩石。
先让我们先弄清楚一件事。 yield from g
的解释相当于for v in g: yield v
甚至没有开始公平地yield from
什么。 因为,让我们面对它,如果所有的yield from
都是扩展for
循环的话,那么就不能保证yield from
语言中增加yield from
并且阻止了Python 2.x中实现的大量新特性。
yield from
提供从caller
到sub-generator
的透明双向信道的yield from
考虑。 这包括从 sub-generator
获取数据和发送数据 。 顺便说一句,如果你不确定发送数据到发生器是什么意思,那么你需要先放下所有东西,然后阅读协程。 戴夫·比兹利(David 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
而不是手动迭代reader()
,我们可以yield from
它yield from
。
def reader_wrapper(g): yield from g
这是有效的,我们消除了一行代码。 而且这个意图可能更清楚一点(或者不是)。 但是没有任何生活改变
使用第一部分的收益率向发生器(协同程序)发送数据
现在让我们做一些更有趣的事情。 让我们创build一个名为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
包装需要接受发送给它的数据(显然),并且在for循环耗尽时也应该处理StopIteration
。 显然只是for x in coro: yield x
做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部分 – exception处理
让我们把它变得更复杂。 如果我们的作者需要处理exception呢? 比方说, 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)
只会引发exception,一切都会崩溃。 我们让它工作,但手动处理exception并发送它们或将它们扔到子生成器( 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
透明的yield from
处理发送值或抛出值到子发生器。
尽pipe如此,这仍然不包括所有的angular落案例。 如果外部发电机closures会发生什么? 如果子生成器返回一个值(是的,在Python 3中,生成器可以返回值),那么应该如何传播返回值呢? 所有yield from
手柄透明处理的angular落案例真是令人印象深刻 。 yield from
神奇的作品中产生并处理所有这些情况。
我个人觉得yield from
是一个糟糕的关键词select,因为它不会使双向性显而易见。 还有其他关键字提出了(像delegate
但被拒绝,因为添加一个新的关键字语言比结合现有的要难得多。
综上所述,最好是把caller
作为一个transparent two way channel
的来源和sub-generator
之间的yield from
。
参考文献:
- PEP 380 – 委托给子生成器的语法(Ewing)[v3.3,2009-02-13]
- PEP 342 – 通过增强型生成器生成协程(GvR,Eby)[v2.5,2005-05-10]
“收益来源”有用的情况是什么?
每一个你有这样一个循环的情况:
for x in subgenerator: yield x
正如PEP所描述的,这是一个使用子生成器的非常天真的尝试,它缺less了几个方面,尤其是PEP 342引入的.throw()
/ .send()
/ .close()
机制的正确处理。 要做到这一点, 相当复杂的代码是必要的。
什么是经典用例?
考虑你想从recursion数据结构中提取信息。 假设我们想要获得树中的所有叶节点:
def traverse_tree(node): if not node.children: yield node for child in node.children: yield from traverse_tree(child)
更重要的是,直到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)
现在你决定把这些循环分解成单独的发生器。 如果没有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)
为什么它与微细线相比?
我认为PEP中的这个部分所讨论的是每一个发生器都有自己独立的执行上下文。 再加上使用yield
和__next__()
分别在生成器迭代器和调用者之间切换执行的事实,这与线程类似,在这里线程和执行上下文(操作系统不时地切换执行线程)堆栈,寄存器…)。
这样做的效果也是可比的:生成器迭代器和调用者都在执行状态同时进行,它们的执行是交错的。 例如,如果生成器执行某种计算,并且调用者打印出结果,则只要这些结果可用,就会看到结果。 这是一种并发的forms。
这个比喻并不是特定的东西,但它是Python中生成器的一个普遍特性。
无论你从发电机内部调用发电机,都需要一个“泵”来重新yield
这些值: for v in inner_generator: yield v
。 正如PEP所指出的那样,大多数人忽视了这种复杂性。 非本地stream量控制如throw()
是PEP中给出的一个例子。 yield from inner_generator
的新语法yield from inner_generator
被用在任何你以前写过显式循环的地方。 不过这不仅仅是语法上的糖,它处理所有被for
循环忽略的边界情况。 “含糖”鼓励人们使用它,从而获得正确的行为。
讨论中的这个消息谈到了这些复杂性:
通过PEP 342引入的附加生成器function,情况不再如此:如Greg的PEP所述,简单迭代不能正确支持send()和throw()。 支持send()和throw()的体操实际上并不是那么复杂,但它们也不是微不足道的。
除了观察发生器是否是一种平行操作之外,我不能用微线程来比较 。 您可以将暂停的生成器视为一个线程,通过yield
将值发送到消费者线程。 实际的实现可能不是这样的(实际的实现显然是Python开发人员非常感兴趣的),但这并不关心用户。
语法中的新yield from
不会为线程添加任何额外的function,只是使正确使用现有function变得更容易。 或者更确切地说,它使得由专家编写的复杂的内部生成器的新手消费者更容易通过该生成器,而不会破坏其任何复杂的function。
一个简短的例子可以帮助你理解用例的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]]])))
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
正如你所看到的,它删除了一个纯Python循环。 这几乎是所有事情,但链式迭代器是Python中一个非常常见的模式。
线程基本上是一个function,允许你跳出完全随机点的function,并跳回到另一个function的状态。 线程pipe理器经常这样做,所以程序似乎同时运行所有这些function。 问题是这些点是随机的,所以你需要使用locking来防止pipe理者在有问题的地方停止function。
在这个意义上,生成器与线程非常相似:它们允许你指定特定的点(只要它们yield
),你可以跳进去和跳出。 当这样使用时,发生器被称为协程。
有关更多详细信息,请阅读有关Python中的协程的优秀教程