在阅读了answer1和answer2之后,yield的目的看起来仍然不清楚。
在第一种情况下,使用下面的函数,
1 2 3 4
| def createGenerator():
mylist = range(3)
for i in mylist:
yield i*i |
调用下面的createGenerator,
埃多克斯1〔2〕
应返回类型为collections.abc.Generator的对象(如(x*x for x in range(3)),是-a collections.abc.Iterator和collections.abc.Iterable。
迭代myGenerator对象,得到第一个值(0)。
埃多克斯1〔9〕
实际上会使createGenerator函数的for循环在内部调用__iter__(myGenerator)并检索collections.abc.Iterator类型的对象(obj表示),然后调用__next__(obj)获取第一个值(0然后使用yield关键字暂停for循环
如果上述理解是正确的,那么,
然后,执行以下语法(第二种情况),
1 2 3 4
| 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语法非常清晰。
- 每次调用next时,尽量让createGenerator返回的东西接受新的信息,这样你就会明白为什么会有收益。在您给出的示例中,您知道在编写代码时希望让生成器输出的内容,但有时(通常)您需要能够将内容传递到生成器中,让它计算一些内容,并生成新的计算内容。
- 同样的原因,我们使用def而不是试图用lambda编写所有函数。同样的原因,我们并不是用列表理解来创建每个列表。genexps在语法上非常有限;它们不能表达太多。
- 例如,也许您的算法更好地用递归表示。虽然有很多方法可以在生成器表达式中实现这一点,但它们绝对不是我认为可读的。
- 在Python中递归通常不是一个好的选择。只有当递归深度受到限制时(如数组的维数)。我想内建的Max系大约有50个。
- @当然,hpaulj不应该像haskell或scala那样编写python,但这并不意味着递归不具备最直接实现某些东西的关键用例。另外,如果您的算法是对数的,那么您可能不会达到递归限制(默认为1000)。看看我刚刚发布的示例。这是使用递归的一个很好的例子。算法非常清晰,如果数据结构嵌套在接近1000层的任何地方,则会遇到其他问题…
- @所以,看看这个问题的答案。接受的答案使用递归,但您始终可以将递归实现编写为迭代实现-只需使用您自己的堆栈!下一个问题说明了这一点。然而,看看它变得多么复杂。我每天都要在16号线以上走5条线,而取舍是"你可以走1000多深"。
尝试在没有yield的情况下进行此操作。
1 2 3 4 5 6 7 8 9 10 11
| 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 |
发电机有两个重要特点:
发电机某种状态(x的值)。由于这种状态,该生成器最终可以返回任意数量的结果,而无需使用大量内存。
由于状态和yield,我们可以向生成器提供用于计算下一个输出的信息。当我们调用send时,该值被分配给y。
我认为没有yield这是不可能的。也就是说,我非常肯定,用生成器函数可以做的任何事情也可以用类来完成。
下面是一个类的例子,它执行完全相同的操作(python 2语法):
1 2 3 4 5 6 7 8 9 10
| 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请参阅最新的编辑,以获取行为与生成器函数完全相同的类的示例。也许这会给你一个关于如何可视化生成器的心理模型,正如你所要求的。
- 这个答案说,每个yield被清单append()取代。那么,如何编写一个替换yield关键字的函数呢?
- @过度交换你链接的答案充其量是不完整的,我会说它实际上是完全错误的。这个答案并不能说明如何将send的效果转化为发电机。请再看一遍我的答案。
- 对于这样的例子,如果没有机会使用send(),用append()替代可能是有意义的。
- @即使是附加的过度交换也与生成器非常不同。假设我想要一个生成器,它将序列1, 2, 3, ...永远变为无穷大。显然,我们不能将所有的正整数附加到一个列表中,因为我们的计算机没有无限的内存。这是生成器最简单的好处:我们根据需要计算值,而不是同时计算所有值。
- 我可以使用类语法处理所有这些情况。正如你在答案中提到的。那么,为什么我需要带有yield关键字的生成器函数呢?编码风格方面?
- @过度交换看看我答案中的类和函数。类的行数已经是函数的两倍,这是在没有实现__iter__的情况下实现的。是的,我认为生成器函数最终只是一个很好的语法。但是请注意,1)这个好的语法确实很方便,2)在某些情况下,yield关键字实际上做了一些我根本不知道的类不能做的事情。
- 我读过Pep342,它说协程是一种自然的表达方式。我认为,这不是关于我们是否需要yield关键字,而是代码映射出心理模型。
把收益看成是"懒惰的回报"。在第二个示例中,函数不返回"值生成器",而是返回完全评估的值列表。这可能完全可以接受,具体取决于用例。当处理大量流式数据时,或者处理不立即可用的数据时(考虑异步操作),yield非常有用。
- 哇,我慢了。我开始打字时0个答复。
- 不,它没有,第二个函数也返回一个生成器——只是genexpr是隐式的。
已经有了一个很好的答案,即能够将send数据转换成一个有收益的生成器。考虑到可读性方面的考虑,虽然非常简单,但直接的转换可以作为生成器表达式更易于阅读:
1
| (x + 1 for x in iterable if x%2 == 1) |
使用完整的生成器定义,某些操作更容易阅读和理解。在某些情况下,如果要适应生成器表达式,请尝试以下操作:
1 2 3 4 5 6 7 8 9 10
| >>> 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来实现递归,但这将是一个不可读的混乱。现在假设我有一些任意嵌套的数据结构,涉及到list和dict,并且我有逻辑来处理这两种情况……你明白我的意思了。
- yield from不在py2上运行。这让我很恼火,因为它不必要地添加到了argparse的py3版本(用yield from x替换for i in x: yield i)。
- @hpaulj我喜欢yield from语法。我觉得这是一种很脓的东西。虽然我在Py2上学会了爱Python,但我现在已经完全接受了Py3。
生成器的功能和生成器的理解基本相同-都会生成生成器对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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] |
g和gl具有相同的属性;产生相同的值;以相同的方式耗尽。
就像列表理解一样,在显式循环中,有一些事情是你无法理解的。但如果理解起作用,就用它。生成器是在2.2版左右添加到python的。生成器的理解是更新的(并且可能使用相同的底层机制)。
在py3-range或py2-xrange中,一次生成一个值,而不是整个列表。它是一个range对象,不是一个生成器,但工作方式大致相同。Py3以其他方式对此进行了扩展,例如字典keys和map。有时这是一种方便,有时我忘了把它们包在list()里。
yield可以更为详细,允许为来电者提供"反馈"。例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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状态。所以它是一个保持内部状态的函数,可以用next或for迭代反复调用。使用send和yield from等工具,generators可以更加复杂。
通常一个函数运行到完成,然后返回。对函数的下一个调用独立于第一个调用,除非使用全局或容易出错的默认值。
https://www.python.org/dev/peps/pep-0289/是用于生成器表达式的pep,来自v 2.4。
This PEP introduces generator expressions as a high performance, memory efficient generalization of list comprehensions [1] and generators [2] .
号
https://www.python.org/dev/peps/pep-0255/pep用于发电机,v.2.2
- 我认为需要注意的是,与yield相关的一个主要特性是,它将数据引入生成器。这与显式循环和理解之间的区别是一致的,因为显式循环可以有一行等待I/O或其他内容。
- 甚至我也必须查找语法来更详细地使用yield——比如我的新示例使用send重置计数器。