实际上,“yield from”的主要用途是什么?Python 3.3?

我现在很难理解PEP 380

  1. 在什么情况下yield from是有用的?
  2. 经典用例是什么?
  3. 为什么将其与微线程进行比较?

到目前为止,我使用过生成器,但从未真正使用过协程(由pep - 342引入)。尽管有一些相似之处,生成器和协程基本上是两个不同的概念。理解协程(不仅仅是生成器)是理解新语法的关键。

恕我直言,大多数书都让它看起来毫无用处,毫无趣味。


谢谢你的精彩回答,但特别感谢agf和他的评论链接到David Beazley的演讲

217866 次浏览

在什么情况下“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)

更重要的是,在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中生成器的一般属性。

无论你在生成器中调用生成器,你都需要一个“泵”来重新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语法并没有在线程方面为语言添加任何额外的功能,它只是使正确使用现有功能更加容易。或者更准确地说,它使新手消费者更容易使用专家编写的复杂内部生成器,而不会破坏生成器的任何复杂特性。

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时),您可以在那里插入和退出。当以这种方式使用时,生成器称为协程。

阅读这个关于Python协程的优秀教程以获得更多细节

让我们先解决一件事。yield from gfor v in g: yield v难道这还不够公正吗等价于yield from的解释。因为,让我们面对现实吧,如果yield from所做的只是扩展for循环,那么它就不能保证将yield from添加到语言中,并排除在Python 2.x中实现一大堆新特性。

yield from所做的是在调用者和子生成器之间建立一个透明的双向连接:

  • 连接是“透明的”,因为它将正确地传播所有内容,而不仅仅是正在生成的元素(例如,异常被传播)。

  • 该连接是“双向”的,即数据既可以从发送,也可以从发送到

(# EYZ1)

顺便说一句,如果你不确定向生成器发送数据是什么意思,你需要放下一切,先阅读协同程序——它们非常有用(与子例程相比),但不幸的是,在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(),我们可以只yield from它。

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。显然,只做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处理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+中,生成器可以返回值),该如何传播返回值?# EYZ2。yield from可以神奇地工作并处理所有这些情况。

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

总之,最好将yield from看作调用者和子生成器之间的transparent two way channel

引用:

  1. PEP 380 -委托给子生成器的语法(Ewing) [v3.3, 2009-02-13]
  2. <李> # EYZ0 - 通过增强生成器(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具有类似的行为。两者都用于暂停协程的执行。

对于Asyncio,如果不需要支持旧的Python版本(即>3.5),async def/await是定义协程的推荐语法。因此,协程中不再需要yield from

但一般来说,在asyncio之外,yield from <sub-generator>在迭代sub-generator时还有一些其他用途,就像前面的回答中提到的那样。

这段代码定义了一个函数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 from使代码更简单、更清晰。

简单地说,yield from为迭代器函数提供了尾递归

yield将产生单个值到集合中。

yield from将产生一个集合到另一个集合,并使它变平。

请看这个例子:

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产生一个生成器,直到生成器为空,然后是继续执行以下代码行

如。

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的主要用例之一