使用yield from共享生成器时,为何看似等价的代码会出现StopIteration异常差异?
嘿,这个问题的核心其实藏在Python生成器的一个关键特性里——生成器是有状态的,而且一旦被迭代,它的状态会持续保留,不会自动重置。再加上你这里所有的func生成器都绑定了同一个全局gen对象,这两种写法的差异就由此而来了。
咱们逐个拆解:
先看案例1:保存生成器对象复用
gen = (x for x in range(10)) def func(): yield from gen F21 = func() F22 = func() next(F21) # 输出0,gen的内部指针推进到下一个元素 next(F22) # 输出1,gen的指针继续向前
这里的F21和F22是两个独立的生成器对象,但它们共享同一个gen生成器的状态。当你调用next(F21)时,yield from会委托gen产出第一个元素(0),此时gen的内部状态已经变了——它现在指向的是下一个要产出的元素1;之后调用next(F22),它同样会委托这个已经被推进过的gen,自然就拿到了1。只要gen还没耗尽(也就是还没产出完10个元素),不管你用F21还是F22调用next,都能拿到下一个元素,不会提前触发StopIteration。
再看案例2:每次创建新生成器
gen = (x for x in range(10)) def func(): yield from gen next(func()) # 输出0,gen指针推进到1 next(func()) # 正常应该输出1,除非gen已经被耗尽
你说这个案例会抛出StopIteration,其实正常情况下前10次调用next(func())都不会有问题——每次调用func()都会生成一个新的生成器对象,但这个新对象依然会绑定那个全局的gen,所以每次next都会推进gen的状态,依次产出0、1、2...直到第10次产出9。第11次调用next(func())时,gen已经没有元素可以产出了,才会抛出StopIteration。
那为什么你觉得两个案例有差异?大概率是你误以为案例2中每次调用func()都会“重置”迭代,从头开始产出0,但实际上因为gen是一个已创建的生成器对象(不是每次调用都生成新生成器的函数),所有func生成器都共享它的状态,所以迭代是连续的。
反过来,如果把gen改成一个返回新生成器的函数,比如:
def gen(): yield from range(10) def func(): yield from gen()
这时候案例2每次调用next(func())都会从头产出0,永远不会提前抛出StopIteration(除非你调用超过10次同一个生成器对象),但这就是另一种场景了。
总结一下:
- 生成器对象的状态是全局共享的,一旦被迭代就会持续推进;
- 案例1和案例2的本质差异不是保存不保存生成器对象,而是你是否理解所有
func生成器都在复用同一个gen的状态; - 只有当
gen被完全耗尽(产出完所有10个元素)时,后续的next才会触发StopIteration。
备注:内容来源于stack exchange,提问作者Hawii Hawii

