yield from 和 yield in python 3.3.2+ 有什么区别

what's the difference between yield from and yield in python 3.3.2+

在 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文档却很复杂。我的问题是,与之前的 yield 语句相比,还有其他区别吗?谢谢

这是一个说明它的例子:

>>> 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 产生可迭代本身。

乍一看,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 在另一个可迭代对象中发布每个元素,从调用第一个生成器的代码的角度来看,就好像该项目来自第一个生成器一样。

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

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

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 - 当那个产生一个值时,在将值获取到使用这些值的实际代码之前,还有另一个中间上下文切换到生成器 1。

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

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

从程序员的角度来看,好像代码是 运行 直接的,没有中断。从流程上看,协程在昂贵的调用点暂停,其他(可能并行调用同一个协程)继续运行。

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

@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/

中有描述

对于大多数应用程序,yield from 只会按顺序从左边的可迭代对象中生成所有内容:

def iterable1():
    yield 1
    yield 2

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

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

对于看到此内容的 90% 的用户 post,我猜这对他们来说已经足够解释了。 yield from 简单地 委托给右侧的可迭代对象。


协程

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

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

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

旁白:您可能想知道这个用例是什么(而且您并不孤单)。一个例子是 contextlib.contextmanager 装饰器。协程也可用于并行化某些任务。我不知道有多少地方利用了它,但是 google 应用引擎的 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真正将(一切!)委托给底层生成器。


Return 语义

请注意,有问题的 PEP 还更改了 return 语义。虽然不是直接在 OP 的问题中,但如果您愿意的话,值得快速离题。在 python2.x 中,您不能执行以下操作:

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

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

 raise StopIteration('done')

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