我很难理解PEP 380。
在什么情况下yield from是有用的? 经典用例是什么? 为什么将其与微线程进行比较?
到目前为止,我使用过生成器,但从未真正使用过协程(由PEP-342引入)。尽管有一些相似之处,生成器和协程基本上是两个不同的概念。理解协程(不仅仅是生成器)是理解新语法的关键。
以我之见,协程是Python中最晦涩的特性,大多数书籍都让它看起来毫无用处和无趣。
感谢这些精彩的回答,但特别感谢agf和他的评论链接到David Beazley的演讲。
我很难理解PEP 380。
在什么情况下yield from是有用的? 经典用例是什么? 为什么将其与微线程进行比较?
到目前为止,我使用过生成器,但从未真正使用过协程(由PEP-342引入)。尽管有一些相似之处,生成器和协程基本上是两个不同的概念。理解协程(不仅仅是生成器)是理解新语法的关键。
以我之见,协程是Python中最晦涩的特性,大多数书籍都让它看起来毫无用处和无趣。
感谢这些精彩的回答,但特别感谢agf和他的评论链接到David Beazley的演讲。
在什么情况下“yield from”是有用的?
你有这样一个循环的每一种情况:
for x in subgenerator:
yield x
正如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)
更重要的是,在生成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__()在生成器-迭代器和调用者之间切换执行,这类似于线程,其中操作系统不时切换执行线程,以及执行上下文(堆栈,寄存器,…)。
这样做的效果也是类似的:生成器-迭代器和调用者都在执行状态下同时进行,它们的执行是交错的。例如,如果生成器执行某种计算,而调用者打印出结果,那么只要结果可用,您就会看到结果。这是并发的一种形式。
不过,这个类比并不是什么特别的东西,而是Python中生成器的一般属性。
Wherever you invoke a generator from within a generator you need a "pump" to re-yield the values: for v in inner_generator: yield v. As the PEP points out there are subtle complexities to this which most people ignore. Non-local flow-control like throw() is one example given in the PEP. The new syntax yield from inner_generator is used wherever you would have written the explicit for loop before. It's not merely syntactic sugar, though: It handles all of the corner cases that are ignored by the for loop. Being "sugary" encourages people to use it and thus get the right behaviors.
这条讨论线程中的消息讨论了这些复杂性:
对于PEP 342引入的附加生成器特性,这是不可能的 更长的情况:正如Greg的PEP中所描述的,简单的迭代不会 正确支持send()和throw()。体操需要支持 当您破坏Send()和throw()时,它们实际上并不复杂 向下,但它们也不是微不足道的。
我不能与微线程进行比较,只能观察到生成器是一种并行。您可以将挂起生成器视为一个线程,它通过yield将值发送给消费线程。实际的实现可能不是这样的(Python开发人员显然对实际的实现非常感兴趣),但这与用户无关。
语法带来的新成果并没有在线程方面为语言增加任何额外的功能,它只是使正确使用现有功能变得更容易。或者更准确地说,它使专家编写的复杂内部生成器的新手更容易通过该生成器,而不会破坏其任何复杂特性。
Yield从基本链迭代器以有效的方式:
# 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协程的优秀教程,了解更多细节
让我们先解决一件事。从g中得到的收益与从g中得到的v是等价的:从v中得到的收益甚至没有开始公正地解释从g中得到的收益是关于什么的。因为,让我们面对它,如果所有的yield from does都是扩展for循环,那么它就不能保证将yield from添加到语言中,并排除在Python 2.x中实现一大堆新特性。
yield from所做的是在调用者和子生成器之间建立一个透明的双向连接:
连接是“透明的”,因为它将正确地传播所有内容,而不仅仅是正在生成的元素(例如,异常被传播)。 这种连接是“双向的”,即数据既可以从生成器发送,也可以发送到生成器。
(如果我们谈论的是TCP, yield from g可能意味着“现在暂时断开我客户端的套接字,并将其重新连接到另一个服务器套接字”。)
顺便说一句,如果你不确定将数据发送到生成器意味着什么,你需要先放弃所有东西,阅读协同例程——它们非常有用(与子例程相比),但不幸的是,在Python中鲜为人知。Dave Beazley的《关于协程的好奇课程》是一个很好的开始。阅读幻灯片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(),而不是手动迭代它。
def reader_wrapper(g):
yield from g
这是可行的,我们减少了一行代码。这样的意图可能会更清楚一些(也可能不是)。但没有改变生活。
使用第1部分中的yield将数据发送到生成器(协程)
现在让我们做一些更有趣的事情。让我们创建一个名为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。很明显,只是在coro中为x做:屈服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部分-异常处理
让我们把它变得更复杂。如果我们的编写器需要处理异常怎么办?让我们假设编写器处理了一个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)会引发异常,所有事情都会戛然而止。让我们让它工作,但手动处理异常并将它们发送或抛出到子生成器(写入器)
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看作调用者和子生成器之间的透明双向通道。
引用:
PEP 380 -委托给子生成器的语法(Ewing) [v3.3, 2009-02-13] Pep 342 - 基于增强生成器的协同程序(GvR, Eby) [v2.5, 2005-05-10]
一个简短的例子将帮助您理解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]]])))
在异步IO协程的实际使用中,yield from与协程函数中的await具有类似的行为。两者都用于暂停协程的执行。
Yield from由基于生成器的协程使用。 Await用于async def协程。(Python 3.5+开始)
对于Asyncio,如果不需要支持旧的Python版本(即>3.5),async def/await是定义协程的推荐语法。因此,yield from在协程中不再需要。
但一般来说,在asyncio之外,yield from <子生成器>在迭代子生成器时仍有一些其他用途,如前面的回答中所述。
这段代码定义了一个函数fixed_sum_digits,返回一个枚举所有6位数字的生成器,使得数字和为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将产生单个值到集合中。
从一个集合到另一个集合,让它变平。
请看这个例子:
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)
在控制台,你会看到:
A
B
C
A
B
C
A
B
C
Yield from生成一个生成器,直到生成器为空,然后继续执行以下代码行。
e.g.
def gen(sequence):
for i in sequence:
yield i
def merge_batch(sub_seq):
yield {"data": sub_seq}
def modified_gen(g, batch_size):
stream = []
for i in g:
stream.append(i)
stream_len = len(stream)
if stream_len == batch_size:
yield from merge_batch(stream)
print("batch ends")
stream = []
stream_len = 0
运行这个程序会得到:
In [17]: g = gen([1,2,3,4,5,6,7,8,9,10])
In [18]: mg = modified_gen(g, 2)
In [19]: next(mg)
Out[19]: {'data': [1, 2]}
In [20]: next(mg)
batch ends
Out[20]: {'data': [3, 4]}
In [21]: next(mg)
batch ends
Out[21]: {'data': [5, 6]}
In [22]: next(mg)
batch ends
Out[22]: {'data': [7, 8]}
In [23]: next(mg)
batch ends
Out[23]: {'data': [9, 10]}
In [24]: next(mg)
batch ends
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Input In [24], in <cell line: 1>()
----> 1 next(mg)
StopIteration:
因此,yield from可以从另一个生成器获取输出,做一些修改,然后将自己的输出作为生成器本身提供给其他生成器。
在我看来,这是yield from的主要用例之一