2013-05-29 3 views
11

Вдохновленный my own answer, я даже не понял, как он работал сам, необходимо учитывать следующее:выражение Генератор никогда не поднимает StopIteration

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


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

Я ожидал 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

+2

Вдохновленный моими комментариями. Я знаю, я так знаменит сейчас. – TerryA

ответ

3

Разработчики решили, что это было ошибкой, поскольку оно может маскировать неясные ошибки. Из-за этого принятие 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 с эквивалентным выражением генератора и, по-видимому, делает это would cause large performance penalties, что полномочия, которые считаются запретительными.

+0

Довольно интересное - и смущает. Спасибо за указание «несогласованности» с Py3 и списком comps ... – glglgl

+0

Ваш последний пример распространяет исключение, потому что он создает список перед вызовом 'any'. Ничто внутри '[]' не оценивается после ввода 'any'. – immibis

4

Интересное поведение, но совершенно понятно.

Если вы превращаете ваше выражение генератора к генератору:

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 

и сделать

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

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

+0

Это не совсем то же самое: 'StopIteration', созданный генератором, всегда будет появляться в собственном вызове цикла' next' и никогда в эффективном теле цикла. Вместо этого попробуйте 'for _ в диапазоне (10): raise StopIteration' - вы получите трассировку. – lvc

+0

ах, это имеет смысл теперь, я думаю, я думал, что моя функция 'F' была генератором для второго – jamylak

+0

@lvc. Вы получаете трассировку, да, но если она используется внутри генератора, она будет поймана в этом контексте. – glglgl

Смежные вопросы