在Python中循环使用相同的生成器安全环路中的生成器?

Fun*_*uit 8 python generator

据我所知,for x in a_generator: foo(x)Python中的循环大致相当于:

try:
    while True:
        foo(next(a_generator))
except StopIteration:
    pass
Run Code Online (Sandbox Code Playgroud)

这表明这样的事情:

for outer_item in a_generator:
    if should_inner_loop(outer_item):
        for inner_item in a_generator:
            foo(inner_item)
            if stop_inner_loop(inner_item): break
    else:
        bar(outer_item)
Run Code Online (Sandbox Code Playgroud)

会做两件事:

  1. 不提出任何例外,段错误或类似的东西
  2. 遍历y,直到它到达一些x地方should_inner_loop(x)返回truthy,然后在它循环在内部for,直到stop_inner_loop(thing)返回true.然后,外环恢复内部停止的位置.

从我公认的不太好的测试来看,它似乎如上所述.但是,我在规范中找不到任何保证此行为在解释器中保持不变的内容.有没有说过或暗示我可以确定它总是这样的?它会导致错误,还是以其他方式执行?(即做一些不同于上述内容的事情


NB以上代码取自我自​​己的经验; 我不知道它是否真的准确.这就是我要问的原因.

Val*_*ntz 6

TL; DR:CPython是安全的(但是我找不到任何相关规范),尽管它可能不会做你想做的事情.


首先,让我们谈谈你的第一个假设,即等价.

for循环实际上首先调用iter()对象,然后运行next()其结果,直到它得到一个StopIteration.

这是相关的字节码(Python的低级形式,由解释器本身使用):

>>> import dis
>>> def f():
...  for x in y:
...   print(x)
... 
>>> dis.dis(f)
  2           0 SETUP_LOOP              24 (to 27)
              3 LOAD_GLOBAL              0 (y)
              6 GET_ITER
        >>    7 FOR_ITER                16 (to 26)
             10 STORE_FAST               0 (x)

  3          13 LOAD_GLOBAL              1 (print)
             16 LOAD_FAST                0 (x)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 JUMP_ABSOLUTE            7
        >>   26 POP_BLOCK
        >>   27 LOAD_CONST               0 (None)
             30 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

GET_ITER调用iter(y)(它本身调用y.__iter__())并将其结果推送到堆栈上(将其视为一堆本地未命名变量),然后进入循环FOR_ITER,调用next(<iterator>)(它本身调用<iterator>.__next__()),然后执行循环内的代码,然后执行JUMP_ABSOLUTE让执行回来FOR_ITER.


现在,为安全起见:

以下是生成器的方法:https://hg.python.org/cpython/file/101404/Objects/genobject.c#l589 正如您在第617行所见,__iter__()is PyObject_SelfIter的实现,您可以在此处找到它的实现.PyObject_SelfIter简单地返回对象(即生成器)本身.

因此,当您嵌套两个循环时,两者都迭代在同一个迭代器上.而且,正如你所说,他们只是在呼唤next()它,所以它是安全的.

但要小心:内部循环将消耗外部循环不会消耗的项目.即使这是你想要做的,它可能不是很可读.

如果这不是您想要做的,请考虑itertools.tee()缓冲迭代器的输出,允许您迭代其输出两次(或更多).只有当tee迭代器在输出流中保持彼此靠近时,这才有效.如果一个tee迭代器在使用另一个之前将完全耗尽,最好只需调用list迭代器来实现一个列表.