【问题标题】:what's the difference between yield from and yield in python 3.3.2+python 3.3.2+中的yield from和yield有什么区别
【发布时间】:2016-06-01 20:00:36
【问题描述】:

python 3.3.2+之后python支持创建生成器函数的新语法

yield from <expression>

我已经对此进行了快速尝试

>>> def g():
...     yield from [1,2,3,4]
...
>>> for i in g():
...     print(i)
...
1
2
3
4
>>>

使用起来似乎很简单,但PEP 文档很复杂。我的问题是,与之前的收益声明相比,还有其他区别吗?谢谢。

【问题讨论】:

标签: python yield


【解决方案1】:

对于大多数应用程序,yield from 只是按顺序生成左侧可迭代的所有内容:

def iterable1():
    yield 1
    yield 2

def iterable2():
    yield from iterable1()
    yield 3

assert list(iterable2) == [1, 2, 3]

对于看到这篇文章的 90% 的用户,我猜这对他们来说已经足够解释了。 yield from 只是委托到右侧的可迭代对象。


协程

但是,这里还有一些更深奥的生成器情况也很重要。关于生成器的一个鲜为人知的事实是它们可以用作协同程序。这并不常见,但您可以根据需要将数据发送到生成器:

def coroutine():
    x = yield None
    yield 'You sent: %s' % x

c = coroutine()
next(c)
print(c.send('Hello world'))

旁白:您可能想知道这个用例是什么(而且您并不孤单)。一个例子是contextlib.contextmanager 装饰器。协同程序也可用于并行化某些任务。我不知道有多少地方可以利用它,但是 google app-engine 的 ndb 数据存储 API 以一种非常漂亮的方式将它用于异步操作。

现在,假设您将send 数据发送到一个生成器,该生成器正在从另一个生成器产生数据……原始生成器如何得到通知?答案是它在 python2.x 中不需要你自己包装生成器:

def python2_generator_wapper():
    for item in some_wrapped_generator():
        yield item

至少不是没有很多痛苦:

def python2_coroutine_wrapper():
    """This doesn't work.  Somebody smarter than me needs to fix it. . .

    Pain.  Misery. Death lurks here :-("""
    # See https://www.python.org/dev/peps/pep-0380/#formal-semantics for actual working implementation :-)
    g = some_wrapped_generator()
    for item in g:
        try:
            val = yield item
        except Exception as forward_exception:  # What exceptions should I not catch again?
            g.throw(forward_exception)
        else:
            if val is not None:
                g.send(val)  # Oops, we just consumed another cycle of g ... How do we handle that properly ...

使用yield from,这一切都变得微不足道:

def coroutine_wrapper():
    yield from coroutine()

因为yield from 真正将(一切!)委托给底层生成器。


返回语义

请注意,所讨论的 PEP 也会更改返回语义。虽然不是直接在 OP 的问题中,但如果您愿意的话,值得快速题外话。在python2.x中,不能进行以下操作:

def iterable():
    yield 'foo'
    return 'done'

这是一个SyntaxError。随着yield的更新,上述功能不合法。同样,主要用例是协程(见上文)。您可以将数据发送到生成器,它可以神奇地完成它的工作(也许使用线程?),而程序的其余部分则执行其他操作。当流控制传回生成器时,StopIteration 将被提升(这对于生成器结束是正常的),但现在StopIteration 将有一个数据负载。就像程序员写的一样:

 raise StopIteration('done')

现在调用者可以捕获该异常并使用数据负载做一些事情来造福其他人。

【讨论】:

  • 感谢您的出色解释,否则我永远不会知道生成器可以以如此漂亮的方式使用。 :-)
【解决方案2】:

乍一看,yield from 是以下算法的快捷方式:

def generator1():
    for item in generator2():
        yield item
    # do more things in this generator

这几乎等同于:

def generator1():
    yield from generator2()
    # more things on this generator

在英语中:当在一个可迭代对象中使用时,yield from 在另一个可迭代对象中发出每个元素,从调用第一个生成器的代码的角度来看,就好像该项目来自第一个生成器。

创建它的主要原因是允许轻松重构严重依赖迭代器的代码 - 使用普通函数的代码总是可以以很少的额外成本将一个函数块重构为其他函数,然后调用 -划分任务,简化代码的读取和维护,小代码sn-ps的复用性更高——

所以,像这样的大型函数:

def func1():
    # some calculation
    for i in somesequence:
        # complex calculation using i 
        # ...
        # ...
        # ...
    # some more code to wrap up results
    # finalizing
    # ...

可以变成这样的代码,没有缺点:

def func2(i):
    # complex calculation using i 
    # ...
    # ...
    # ...
    return calculated_value

def func1():
    # some calculation
    for i in somesequence:
         func2(i)
    # some more code to wrap up results
    # finalizing
    # ...

然而,当访问迭代器时,表单

def generator1():
    for item in generator2():
        yield item
    # do more things in this generator

for item in generator1():
    # do things

要求对于从 generator2 消耗的每个项目,首先将运行上下文切换到 generator1,在该上下文中什么都不做,并且必须将 cotnext 切换到 generator2 - 当那个产生一个值,在将值传递给使用这些值的实际代码之前,还有另一个中间上下文切换到 generator1。

避免了这些中间上下文切换的产生,如果有很多迭代器链接,这可以节省相当多的资源:上下文直接从消耗最外层生成器的上下文切换到最内层生成器,跳过中间生成器的上下文完全发电机,直到内部的发电机用完为止。

后来,该语言通过中间上下文利用这种“调谐”,将这些生成器用作协同例程:可以进行异步调用的函数。有了适当的框架,如https://www.python.org/dev/peps/pep-3156/ 中所述,这些协同例程的编写方式是,当它们调用一个需要很长时间才能解决的函数时(由于网络操作或 CPU 密集型操作)可以卸载到另一个线程) - 该调用是使用 yield from 语句进行的 - 然后框架主循环进行安排,以便正确调度调用的昂贵函数,并重新执行(框架主循环始终是调用 co -例程本身)。当代价高昂的结果准备好时,框架使被调用的协程表现得像一个耗尽的生成器,并恢复第一个协程的执行。

从程序员的角度来看,代码就像是直接运行,没有中断。从进程的角度来看,协程在代价高昂的调用处暂停,其他(可能是对同一个协程的并行调用)继续运行。

因此,作为网络爬虫的一部分,可以编写一些代码:

@asyncio.coroutine
def crawler(url):
   page_content = yield from async_http_fetch(url)
   urls = parse(page_content)
   ...

当从 asyncio 循环调用时,它可以同时获取数十个 html 页面。

Python 3.4 将 asyncio 模块添加到 stdlib 作为此类功能的默认提供程序。它工作得非常好,以至于在 Python 3.5 中,语言中添加了几个新关键字,以区分协同例程和异步调用与生成器的使用,如上所述。这些在https://www.python.org/dev/peps/pep-0492/

中有描述

【讨论】:

  • 噢……这是我第一次看到 PEP-0492。这是一个非常棒的变化。感谢您指出:-)
  • @mgilson PEP-0492 与 .net 异步和等待语法非常相似。 :)
  • 他们在某处得到它并不奇怪 :-)
【解决方案3】:

这是一个说明它的例子:

>>> def g():
...     yield from range(5)
... 
>>> list(g())
[0, 1, 2, 3, 4]
>>> def g():
...     yield range(5)
... 
>>> list(g())
[range(0, 5)]
>>>

yield from 产生可迭代的每一项,但yield 产生可迭代本身。

【讨论】:

  • 根据 OP 的例子,我怀疑他知道这一点。为了以防万一,仍然值得一说——但我认为 OP 正试图弄清楚为什么 PEP 如此复杂,而这如此简单:-)
猜你喜欢
  • 2020-05-30
  • 1970-01-01
  • 2017-01-09
  • 2014-01-28
  • 2012-12-31
  • 2014-11-11
  • 2010-12-14
  • 2016-09-08
  • 2018-01-19
相关资源
最近更新 更多