问题:实际上,Python 3.3中新的“ yield from”语法的主要用途是什么?
我很难缠住PEP 380。
- 在什么情况下“产生于”有用?
- 什么是经典用例?
- 为什么与微线程相比?
[更新]
现在,我了解了造成困难的原因。我曾经使用过生成器,但从未真正使用过协程(由PEP-342引入)。尽管有一些相似之处,但生成器和协程基本上是两个不同的概念。了解协程(不仅是生成器)是了解新语法的关键。
恕我直言,协程是最晦涩的Python功能,大多数书籍使它看起来毫无用处且无趣。
感谢您做出的出色回答,特别感谢agf及其与David Beazley演讲相关的评论。大卫·罗克。
回答 0
让我们先解决一件事。该解释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以获得快速入门。
使用以下命令从生成器读取数据
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
那行得通,我们消除了一行代码。意图可能会更清晰(或不太清楚)。但是生活没有改变。
使用第1部分中的收益将数据发送到生成器(协程)
现在,让我们做一些更有趣的事情。让我们创建一个名为coroutine的协程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
包装器需要(显然)接受发送给它的数据,并且还应处理StopIteration
for循环用尽时的情况。显然只是做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部分-异常处理将数据发送到生成器收益
让我们使其更加复杂。如果我们的作者需要处理异常怎么办?假设writer
句柄a 遇到一个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)
只是引发了异常,一切都崩溃了。让它正常工作,但手动处理异常并将其发送或将其抛出到子生成器(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
是一个糟糕的关键字选择,因为它不会使双向性变得显而易见。提出了其他关键字(例如delegate
但被拒绝了,因为向该语言添加新关键字比合并现有关键字要困难得多。
总之,最好将其yield from
视为transparent two way channel
调用方和子生成方之间的。
参考文献:
回答 1
在什么情况下“产生于”是有用的?
您遇到这样的循环的每种情况:
for x in subgenerator:
yield x
作为PEP介绍,这是一个相当幼稚企图在使用子发生器,它缺少几个方面,特别是妥善处理.throw()
/ .send()
/ .close()
通过引进机制PEP 342。要正确执行此操作,需要相当复杂的代码。
什么是经典用例?
考虑您要从递归数据结构中提取信息。假设我们要获取树中的所有叶节点:
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__()
分别,这类似于线程,其中操作系统会不时切换执行线程以及执行上下文(堆栈,寄存器, …)。
其效果也相当:生成器迭代器和调用者都同时在其执行状态中进行,它们的执行是交错的。例如,如果生成器进行某种计算,并且调用方打印出结果,则结果可用时,您将立即看到它们。这是一种并发形式。
这种类比不是特定于的yield from
-而是Python中生成器的一般属性。
回答 2
无论您从生成器内部调用生成器的哪个位置,都需要一个“泵”来重新yield
设置值: for v in inner_generator: yield v
。正如PEP所指出的那样,大多数人都忽略了这一点的微妙复杂性。throw()
PEP中提供了一个示例,例如非本地流控制。yield from inner_generator
无论您for
之前编写了显式循环的地方,都将使用新语法。但是,它不仅是语法糖,它还处理了for
循环忽略的所有极端情况。成为“丑闻”会鼓励人们使用它,从而获得正确的行为。
讨论线程中的此消息讨论了以下复杂性:
有了PEP 342引入的其他生成器功能,情况已不再如此:如Greg的PEP中所述,简单的迭代不正确地支持send()和throw()。当分解它们时,支持send()和throw()所需的体操实际上并不那么复杂,但是它们也不是简单的。
除了观察到生成器是一种平行论之外,我无法与微线程进行比较。您可以将挂起的生成器视为通过以下方式发送值的线程:yield
到使用者线程的线程。实际的实现可能并非如此(Python开发人员显然对实际的实现非常感兴趣),但这与用户无关。
新的yield from
语法不会在线程方面为语言增加任何其他功能,而只是使正确使用现有功能更加容易。或更准确地说,它使专家编写的复杂内部生成器的新手消费者可以更轻松地通过该生成器,而不会破坏其任何复杂功能。
回答 3
一个简短的示例将帮助您理解的一个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]]])))
回答 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中很常见的模式。
线程基本上是一种功能,使您可以在完全随机的点跳出函数,然后跳回另一个函数的状态。线程管理器经常执行此操作,因此该程序似乎可以同时运行所有这些功能。问题是这些点是随机的,因此您需要使用锁定来防止主管在有问题的点停止该功能。
在这种意义上,生成器与线程非常相似:它们允许您指定特定点(无论何时, yield
),您可以在其中跳入和跳出。当以这种方式使用时,生成器称为协程。
回答 5
在应用的使用为异步IO协程,yield from
也有类似的行为作为await
在协程功能。两者都用于中止协程的执行。
yield from
由基于生成器的协程使用。
对于Asyncio,如果不需要支持较旧的Python版本(即> 3.5),则建议使用async def
/ await
作为定义协程的语法。因此yield from
,协程中不再需要。
但通常在asyncio之外,如先前答案中所述,yield from <sub-generator>
在迭代子生成器方面还有其他用途。
回答 6
该代码定义了一个函数,该函数fixed_sum_digits
返回一个生成器,该生成器枚举所有六个数字的数字,以使数字的总和为20。
def iter_fun(sum, deepness, myString, Total):
if deepness == 0:
if sum == Total:
yield myString
else:
for i in range(min(10, Total - sum + 1)):
yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)
def fixed_sum_digits(digits, Tot):
return iter_fun(0,digits,"",Tot)
试着不用来写yield from
。如果您找到有效的方法,请告诉我。
我认为对于这种情况:访问树yield from
使代码更简单,更清晰。
回答 7
简而言之,为迭代器函数yield from
提供尾递归。