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演讲相关的评论。戴维岩。
我们先把一件事解决。关于
连接在某种意义上是"透明的",即它也将正确地传播所有内容,而不仅仅是正在生成的元素(例如,传播异常)。
连接是"双向的",从这个意义上说,数据既可以从发电机发送,也可以发送到发电机。
(如果我们谈论的是TCP,那么
顺便说一句,如果您不确定将数据发送到生成器意味着什么,那么首先需要删除所有内容并阅读协程,它们非常有用(与子例程形成对比),但不幸的是,它们在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 |
我们不需要手动迭代
1 2 | def reader_wrapper(g): yield from g |
这是可行的,我们去掉了一行代码。可能意图更清楚(或不清楚)。但生活没有改变。
使用第1部分的产量将数据发送到发电机(协程)现在让我们做些更有趣的事情。让我们创建一个名为
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) |
现在的问题是,包装器函数应该如何处理向编写器发送数据,以便将发送到包装器的任何数据透明地发送到
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循环耗尽时还应该处理
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部分异常处理向发电机发送数据让我们把事情弄复杂一点。如果我们的作者需要处理异常呢?假设
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) |
如果我们不改变
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 |
嗯,这不起作用,因为
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 |
不过,这还没有涵盖所有的角落案件。如果外部发电机关闭会发生什么?当子生成器返回一个值(是的,在Python3.3+中,生成器可以返回值)时,该如何传播返回值?
我个人认为
总之,最好把
参考文献:
What are the situations where"yield from" is useful?
每种情况下都会出现这样的循环:
1 2 | for x in subgenerator: yield x |
正如PEP所描述的,这是一个相当幼稚的使用子生成器的尝试,它缺少几个方面,特别是PEP 342引入的
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) |
更重要的是,在
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) |
现在您决定将这些循环分解成单独的生成器。如果没有
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中的这一部分讨论的是,每个生成器都有自己独立的执行上下文。除了在生成器迭代器和分别使用
这样做的效果也相当:生成器迭代器和调用程序在执行状态下同时进行,它们的执行是交错的。例如,如果生成器进行某种计算,并且调用程序打印出结果,那么一旦结果可用,您就会看到结果。这是一种并发形式。
不过,这种类比并不是
无论从发电机内部调用发电机,都需要一个"泵"来重新输入cx1(6)的值:
讨论主题中的这条消息谈到了这些复杂性:
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.
除了观察到生成器是一种平行性之外,我不能用微线程来进行比较。您可以将挂起的生成器视为一个线程,它通过
新的
会帮助你了解在短期example of S是一
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 is used by the发生器基于协同。P></await is used forasync def 协同。(因为Python 3.5 +)P></
我不asyncio for、if need to an基础支持Python版本(即>3.5),
但在外面asyncio通用
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中是非常常见的模式。
线程基本上是一个特性,它允许您在完全随机的点上跳出函数,并跳转回另一个函数的状态。线程管理器经常这样做,因此程序似乎同时运行所有这些函数。问题是这些点是随机的,所以您需要使用锁定来防止主管在有问题的点停止函数。
从这个意义上讲,生成器与线程非常相似:它们允许您指定特定的点(每当它们
阅读关于Python中协程的优秀教程了解更多详细信息