Подтвердить что ты не робот

Выражение генератора никогда не вызывает StopIteration

Вдохновленный моим собственным ответом, я даже не понял, как он работает сам, рассмотрим следующее:

def has22(nums):
    it = iter(nums)
    return any(x == 2 == next(it) for x in it)


>>> has22([2, 1, 2])
False

Я ожидал, что a StopIteration будет поднято, так как при достижении 2, next(it) будет продвигать потребленный итератор. Однако кажется, что это поведение полностью отключено, только для генераторных выражений! Выражение генератора кажется сразу break после этого.

>>> it = iter([2, 1, 2]); any(x == 2 == next(it) for x in it)
False
>>> it = iter([2, 1, 2]); any([x == 2 == next(it) for x in it])

Traceback (most recent call last):
  File "<pyshell#114>", line 1, in <module>
    it = iter([2, 1, 2]); any([x == 2 == next(it) for x in it])
StopIteration
>>> def F(nums):
        it = iter(nums)
        for x in it:
            if x == 2 == next(it): return True


>>> F([2, 1, 2])

Traceback (most recent call last):
  File "<pyshell#117>", line 1, in <module>
    F([2, 1, 2])
  File "<pyshell#116>", line 4, in F
    if x == 2 == next(it): return True
StopIteration

Даже это работает!

>>> it=iter([2, 1, 2]); list((next(it), next(it), next(it), next(it))for x in it)
[]

Итак, я думаю, мой вопрос в том, почему это поведение включено для выражений генератора?

Примечание: То же поведение в 3.x

4b9b3361

Ответ 1

Разработчики решили, что это ошибка, потому что она может маскировать неясные ошибки. Из-за этого принятие PEP 479 означает, что это уходит.

В Python 3.5, если вы выполняете from __future__ import generator_stop, а по умолчанию в Python 3.7, пример в вопросе завершится с RuntimeError. Вы все равно можете достичь такого же эффекта (позволяя nums не быть предварительно вычисленным) с помощью некоторой магии itertools:

from itertools import tee, islice

def has22(nums):
    its = tee(nums, 2)
    return any(x == y == 2 for x, y in 
               zip(its[0], islice(its[1], 1, None)))

Причина, по которой она когда-либо работала, в первую очередь связана с тем, как работают генераторы. Вы можете думать об этом для цикла:

for a in b:
    # do stuff

Как (примерно) эквивалентно этому:

b = iter(b) 
while True:
    try:
        a = next(b)
    except StopIteration:
        break
    else:
        # do stuff

Теперь все примеры имеют два для объединенных циклов (один в выражении генератора, один в функции, потребляющей его), так что внутренний цикл повторяется один раз, когда внешний цикл выполняет свой вызов next. Что происходит, когда "# do stuff" во внутреннем цикле raise StopIteration?

>>> def foo(): raise StopIteration
>>> list(foo() for x in range(10))
[]

Исключение распространяется из внутреннего цикла, поскольку оно не находится в его защите и попадает в внешний цикл. При новом поведении Python перехватит StopIteration, который собирается размножаться из генератора и заменяет его на RuntimeError, который не будет улавливаться контуром for.

Это также подразумевает, что этот код выглядит следующим образом:

def a_generator():
     yield 5
     raise StopIteration

также потерпит неудачу, и поток списка рассылки создает впечатление, что это все равно считается плохим. Правильный способ сделать это:

def a_generator():
    yield 5
    return

Как вы указали, переписные слова уже ведут себя по-другому:

>>> [foo() for x in range(10)]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
  File "<stdin>", line 1, in foo
StopIteration

Это несколько подробное описание реализации. Проникающие списки не преобразуются в вызов list с эквивалентным выражением генератора и, по-видимому, делают это приведет к большим штрафам за исполнение, что полномочия, которые считаются запретительными.

Ответ 2

Интересное поведение, но абсолютно понятное.

Если вы преобразуете выражение генератора в генератор:

def _has22_iter(it):
    for x in it:
        yield x == 2 and x == next(it)

def has22(nums):
    it = iter(nums)
    return any(_has22_iter(it))

ваш генератор поднимает StopIteration в следующих условиях:

  • функция генератора достигает своего конца
  • есть оператор return где-то
  • есть raise StopIteration где-то

Здесь у вас есть третье условие, поэтому генератор завершается.

Сравните со следующим:

def testgen(x):
    if x == 0:
        next(iter([])) # implicitly raise
    if x == 1:
        raise StopIteration
    if x == 2:
        return

и do

list(testgen(0)) # --> []
list(testgen(1)) # --> []
list(testgen(2)) # --> []
list(testgen(3)) # --> []

во всех случаях вы получаете одинаковое поведение.