为什么python生成器需要yield?

时间:2017-06-15 23:07:20

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

在阅读answer1answer2之后,yield的目的仍然不明确。

在第一种情况下,使用以下功能

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

在下面调用createGenerator

myGenerator = createGenerator()

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

迭代myGenerator个对象并获取第一个值(0),

next(myGenerator)

实际上会使for循环createGenerator函数在内部调用__iter__(myGenerator)并检索collections.abc.Iterator类型对象(obj(比如说​​))然后调用{ {1}}获取第一个值(__next__(obj)),然后使用0关键字暂停for循环

如果这种理解(上图)是正确的,那么,

然后,执行以下语法(第二种情况),

yield

不足以达到同样的目的(上面)并且看起来更具可读性吗? Aren的语法记忆效率都很高吗?如果是,那么,我应该何时使用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的情况?

4 个答案:

答案 0 :(得分:4)

尝试不使用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,我们可以为生成器提供用于计算其下一个输出的信息。当我们调用y时,该值会分配给send

  3. 如果没有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__,但很明显应该如何运作。

答案 1 :(得分:1)

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

答案 2 :(得分:0)

生成器函数和生成器理解基本相同 - 都生成生成器对象:

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可以更精细,允许来电者的“反馈”。 e.g。

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/是生成器表达式的PEP,从v 2.4开始。

  

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

https://www.python.org/dev/peps/pep-0255/ PEP for generators,v.2.2

答案 3 :(得分:0)

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

(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']

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