Python生成器中的代码执行何时停止?

6

我正在尝试通过构建一个类似于“enumerate”内置函数的生成器来理解yield语句的行为,但是我发现在迭代过程中会出现不一致的情况。

def enumerate(sequence, start=0):
n = start
for elem in sequence:
    print("Before the 'yield' statement in the generator, n = {}".format(n))
    yield n, elem
    n += 1
    print("After the 'yield' statement in the generator, n = {}".format(n))

我理解生成器的执行过程是,当程序执行到 yield 语句时,代码执行会暂停并返回一个值。下面的脚本验证了我的理解。

a = 'foo'
b = enumerate(a)
n1,v1 = next(b)
print('n1 = {}, v1 = {}\n'.format(n1,v1))
n2,v2 = next(b)
print('n2 = {}, v2 = {}'.format(n2,v2))

在这种情况下,生成器似乎会在yield语句处停止,并在第二个“next”语句中的n+=1处恢复:
Before the 'yield' statement in the generator, n = 0
n1 = 0, v1 = f

After the 'yield' statement in the generator, n = 1
Before the 'yield' statement in the generator, n = 1
n2 = 1, v2 = o

然而,如果我使用下面的for循环,生成器似乎不会在yield语句处停止。
for n,v in enumerate(a[0:1]):
    print('n = {}, v = {}'.format(n,v))

这是我得到的内容:
Before the 'yield' statement in the generator, n = 0
n = 0, v = f
After the 'yield' statement in the generator, n = 1

考虑评论后进行编辑

我意识到我只迭代了一个元素,但我没有预料到即使我迭代所有元素,也会看到最后一个“在生成器的'yield'语句之后”句子。

print('\n\n')
for n,v in enumerate(a):
    print('n = {}, v = {}'.format(n,v))

Before the 'yield' statement in the generator, n = 0
n = 0, v = f
After the 'yield' statement in the generator, n = 1
Before the 'yield' statement in the generator, n = 1
n = 1, v = o
After the 'yield' statement in the generator, n = 2
Before the 'yield' statement in the generator, n = 2
n = 2, v = o
After the 'yield' statement in the generator, n = 3

为什么会发生这种情况?


1
因为您正在迭代一个元素。 - Ignacio Vazquez-Abrams
输出结果完全符合预期。问题出在哪里? - Mad Physicist
应该解释 print(a[0:1]) - Mad Physicist
如果一个 for 循环在产生第一个值后就停止了,那它就不是一个循环了,对吧?这个循环会一直持续到没有剩余的值为止。 - Aran-Fey
我认为“实现”的概念是让您困惑的地方。解释器在通俗意义上并不真正进行解释。它不会在您明确告诉它之前就“意识到”你完成了某个任务。这也是编程的整个关键所在。 - Mad Physicist
显示剩余4条评论
2个回答

5

这里的根本问题在于,您混淆了一个事实:通过观察生成器,可以知道它何时用尽,但Python只能通过运行代码来确定。当Python遇到您认为是最后一个yield时,它并不知道它是最后一个。如果您的生成器长这样:

def enumeratex(x, start=0):
    for elem in x:
        yield start, x
        start += 1
    yield start, None

在这里,出于某些原因,主生成器循环之后会返回一个最终的None元素。在你执行以下操作之前,Python无法知道生成器何时完成:
  1. 从生成器中返回。
  2. 引发错误,在这种情况下,所有内容都将停止。
在Python 3.7之前的版本中,生成器可以引发StopIteration来表示终止。实际上,一个return语句等效于raise StopIteration(如果返回None)或raise StopIteration(return_value)
因此,虽然你可以自行决定如何告诉Python结束生成器,但必须明确指出。一个yield本身不能结束生成器。 简而言之: 即使最后一个值已经被生成,生成器中循环中的所有代码都将始终运行,因为Python只能通过实际执行所有代码来知道它是最后一个值。

2
你编号列表中的第一点不再正确(它曾经是正确的)。在Python 3.7(或使用from __future__ import generator_stop的3.5+版本)中,未在生成器中捕获的StopIteration将转换为RuntimeError。在新代码中,当你完成时应该总是从生成器中return(尽管通过运行函数末尾隐式地返回None仍然可以)。 - Blckknght
@Blckknght,谢谢你的提醒。已更新。 - Mad Physicist

4
答案在于理解Python的for循环:它获取一个对象的迭代器(即iter()),并一直执行,直到引发一个StopIteration异常。当生成器代码完成时,即获取包含该函数的return语句(也可能是隐式的),就会抛出StopIteration异常。这也是为什么它不会在yield处停止,而会继续请求下一个yield,直到生成器完成。

@MadPhysicist 它不一定是 None。当函数结束时,它会引发 StopIteration。 - Aran-Fey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接