【问题标题】:Why yield is required for python generator?python生成器为什么需要yield?
【发布时间】:2017-11-18 14:04:51
【问题描述】:

看了answer1answer2之后,yield的目的还是不清楚。


在第一种情况下,使用以下函数,

def createGenerator():
   mylist = range(3)
   for i in mylist:
      yield i*i

在下面调用createGenerator

myGenerator = createGenerator()

应该返回collections.abc.Generator类型的对象(如(x*x for x in range(3))),is-a collections.abc.Iterator & collections.abc.Iterable

遍历myGenerator对象并获得第一个值(0),

next(myGenerator)

实际上会使createGenerator 函数的for 循环在内部调用__iter__(myGenerator) 并检索collections.abc.Iterator 类型对象(obj(say))然后调用__next__(obj) 以获取第一个值(0 ) 之后是使用 yield 关键字的 for 循环的暂停


如果这个理解(以上)是正确的,那么,

那么,下面的语法(第二种情况),

def createGenerator():
   return (x*x for x in range(3))
myGen = createGenerator() # returns collections.abc.Generator type object
next(myGen) # next() must internally  invoke __next__(__iter__(myGen)) to provide first value(0) and no need to pause

不足以达到相同的目的(上图)并且看起来更具可读性?两种语法记忆都不是有效的吗?如果是,那么,我应该什么时候使用yield 关键字?是否存在必须使用yield 的情况?

【问题讨论】:

  • 生成器表达式是理解结构。它们对您可以在其中执行的操作类型有更多限制。例如,您不能有复合语句。有一些方法可以解决这个问题,但我认为这类似于“我什么时候应该使用 for 循环”与“我什么时候应该使用理解”。使用当时更具可读性的一种,或者让您的生活更轻松的一种。
  • 当您没有统一的数据要返回时会发生什么?是的,当你的生成器只是一个围绕迭代器的美化包装器时,你不需要 yield,但通常情况并非如此。
  • 不,一点也不。我发现 yield 语法 very 清晰。
  • 尝试让createGenerator返回的东西在每次调用next时都接受新的信息,然后你就会明白为什么会有yield存在。在您给出的示例中,您知道在编写代码时希望生成器吐出的东西,但有时(通常)您需要能够将东西传递到生成器中,让它计算一些东西,然后产生新计算的东西。
  • 同样的原因我们有def,而不是尝试用lambda编写我们所有的函数。同样的原因,我们不会使用列表理解创建每个列表。 Genexp 在语法上非常有限;他们不能表达太多。

标签: python python-3.x generator yield coroutine


【解决方案1】:

尝试在不使用yield 的情况下执行此操作

def func():
    x = 1
    while 1:
        y = yield x
        x += y


f = func()
f.next()  # Returns 1
f.send(3)  # returns 4
f.send(10)  # returns 14

生成器有两个重要特性:

  1. 生成器某些状态(x 的值)。由于这种状态,这个生成器最终可以返回任意数量的结果,而无需使用大量内存。

  2. 由于状态和yield,我们可以为生成器提供用于计算其下一个输出的信息。当我们调用send 时,该值被分配给y

如果没有yield,我认为这是不可能的。 也就是说,我很确定你可以用生成器函数做的任何事情也可以用类来完成。

这是一个做同样事情的类的例子(python 2 语法):

class MyGenerator(object):
    def __init__(self):
        self.x = 1

    def next(self):
        return self.x

    def send(self, y):
        self.x += y
        return self.next()

我没有实现__iter__,但它应该如何工作很明显。

【讨论】:

  • 因此,当您调用 func() 时,Generator 类型对象(比如 obj)会被返回。那个对象(obj)看起来像什么?在该对象上,如果您运行next(),则__next(__iter(obj)) 应由while 在内部调用。在您的答案中,我如何可视化 Generator 类型的对象?在我的查询中,我知道我的Generator 类型对象在这两种情况下都是(x*x for x in range(3))。不是吗?
  • @overexchange 查看最近的编辑以获取行为与生成器函数完全相同的类的示例。也许这会给你一个如何可视化生成器的心智模型,正如你所要求的。
  • 这个answer 说,每个yield 都替换为列表append()。那么,我将如何编写一个替换 yield 关键字的函数呢?
  • @overexchange 您链接的答案充其量是不完整的,我会说它实际上完全是错误的。该答案没有显示如何将sending 的效果放入生成器中。请再次阅读我的回答。
  • 对于这样的examples,可能替换为append() 是有意义的,因为没有机会send()
【解决方案2】:

将收益视为“惰性回报”。在您的第二个示例中,您的函数不返回“值生成器”,而是返回一个完全评估的值列表。根据用例,这可能是完全可以接受的。 Yield 在处理大批量流式数据或处理无法立即获得的数据(想想异步操作)时很有用。

【讨论】:

  • 哇,我慢。我开始打字时有 0 条回复。
  • 不,它没有,第二个函数也返回一个生成器——只是geneexpr是隐式的。
【解决方案3】:

生成器函数和生成器推导基本相同——都产生生成器对象:

In [540]: def createGenerator(n):
     ...:     mylist = range(n)
     ...:     for i in mylist:
     ...:         yield i*i
     ...:         
In [541]: g = createGenerator(3)
In [542]: g
Out[542]: <generator object createGenerator at 0xa6b2180c>

In [545]: gl = (i*i for i in range(3))
In [546]: gl
Out[546]: <generator object <genexpr> at 0xa6bbbd7c>

In [547]: list(g)
Out[547]: [0, 1, 4]
In [548]: list(gl)
Out[548]: [0, 1, 4]

ggl 具有相同的属性;产生相同的值;以同样的方式用完。

就像列表推导式一样,您可以在显式循环中执行一些您无法通过推导式执行的操作。但是,如果理解可以完成工作,请使用它。生成器在 2.2 版左右的某个时候被添加到 Python 中。生成器推导式更新(并且可能使用相同的底层机制)。

在 Py3 range 或 Py2 xrange 中,一次生成一个值,而不是整个列表。这是一个range 对象,不是生成器,但工作方式大致相同。 Py3 以其他方式对此进行了扩展,例如字典 keysmap。有时这很方便,有时我忘记将它们包装在 list() 中。


yield 可以更详细,允许调用者“反馈”。例如

In [564]: def foo(n):
     ...:     i = 0
     ...:     while i<n:
     ...:         x = yield i*i
     ...:         if x is None:
     ...:             i += 1
     ...:         else:
     ...:             i = x
     ...:             

In [576]: f = foo(3)
In [577]: next(f)
Out[577]: 0
In [578]: f.send(-3)    # reset the counter
Out[578]: 9
In [579]: list(f)
Out[579]: [4, 1, 0, 1, 4]

我认为生成器操作的方式是创建使用代码和初始状态初始化对象。 next() 将其运行到 yield,并返回该值。下一个next() 让它再次旋转,直到它遇到yield,依此类推,直到它遇到stop iteration 条件。所以它是一个维护内部状态的函数,可以通过nextfor 迭代重复调用。使用sendyield from 等等generators 可以更加复杂。

通常一个函数会一直运行到完成,然后返回。对函数的下一次调用独立于第一次调用——除非您使用全局变量或容易出错的默认值。


https://www.python.org/dev/peps/pep-0289/ 是从 v 2.4 开始的生成器表达式的 PEP。

这个 PEP 引入了生成器表达式,作为列表推导 [1] 和生成器 [2] 的高性能、内存高效的泛化。

https://www.python.org/dev/peps/pep-0255/ 生成器的 PEP,v.2.2

【讨论】:

  • 我认为重要的是要注意与yield 相关的一个主要功能是将数据带入生成器。这与显式循环和推导式之间的区别是一致的,因为显式循环可以有一行等待 I/O 或其他。
  • 即使我必须查找语法以更详细地使用 yield - 例如我使用 send 重置计数器的新示例。
【解决方案4】:

关于将send 数据放入具有产量的生成器的能力已经有一个很好的答案。仅考虑可读性,虽然简单、直接的转换作为生成器表达式当然更具可读性:

(x + 1 for x in iterable if x%2 == 1)

使用完整的生成器定义,某些操作更易于阅读和理解。某些情况下适合生成器表达式令人头疼,请尝试以下操作:

>>> x = ['arbitrarily', ['nested', ['data'], 'can', [['be'], 'hard'], 'to'], 'reach']
>>> def flatten_list_of_list(lol):
...     for l in lol:
...         if isinstance(l, list):
...             yield from flatten_list_of_list(l)
...         else:
...             yield l
...
>>> list(flatten_list_of_list(x))
['arbitrarily', 'nested', 'data', 'can', 'be', 'hard', 'to', 'reach']

当然,您也许可以使用lambdas 编写一个适合单行的解决方案来实现递归,但这将是一个难以理解的混乱。现在想象一下,我有一些涉及listdict 的任意嵌套数据结构,并且我有处理这两种情况的逻辑......你明白了我的意思。

【讨论】:

  • yield from 不在 Py2 上运行。它让我感到烦恼的是,它被不必要地添加到了 argparse 的 Py3 版本中(用 yield from x 替换 for i in x: yield i)。
  • @hpaulj 我喜欢yield from 语法。我认为它非常 Pythonic。虽然我在 Py2 上学会了爱上 Python,但现在我已经完全接受了 Py3。
猜你喜欢
  • 2016-12-02
  • 1970-01-01
  • 2020-04-07
  • 1970-01-01
  • 2014-02-16
  • 1970-01-01
  • 2023-04-01
  • 1970-01-01
  • 2015-01-14
相关资源
最近更新 更多