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

Почему исключения в генераторе Python не пойманы?

У меня есть следующий экспериментальный код, функция которого аналогична встроенному zip. То, что он пытается сделать, должно было быть простым и ясным, пытаясь вернуть заархивированные кортежи по одному, пока не произойдет IndexError, когда мы остановим генератор.

def my_zip(*args):
    i = 0
    while True:
        try:
            yield (arg[i] for arg in args)
        except IndexError:
            raise StopIteration
        i += 1

Однако, когда я попытался выполнить следующий код, IndexError не был пойман, а вместо него был добавлен генератором:

gen = my_zip([1,2], ['a','b'])
print(list(next(gen)))
print(list(next(gen)))
print(list(next(gen)))


IndexError                                Traceback (most recent call last)
I:\Software\WinPython-32bit-3.4.2.4\python-3.4.2\my\temp2.py in <module>()
     12 print(list(next(gen)))
     13 print(list(next(gen)))
---> 14 print(list(next(gen)))

I:\Software\WinPython-32bit-3.4.2.4\python-3.4.2\my\temp2.py in <genexpr>(.0)
      3     while True:
      4         try:
----> 5             yield (arg[i] for arg in args)
      6         except IndexError:
      7             raise StopIteration
IndexError: list index out of range

Почему это происходит?

Изменить:

Спасибо @thefourtheye за предоставление приятного объяснения того, что происходит выше. Теперь возникает другая проблема:

list(my_zip([1,2], ['a','b']))

Эта строка никогда не возвращается и, кажется, повесит машину. Что происходит сейчас?

4b9b3361

Ответ 1

yield дает объект-генератор каждый раз, и когда генераторы были созданы, проблем не было вообще. Вот почему try...except в my_zip ничего не ловит. В третий раз, когда вы его выполнили,

list(arg[2] for arg in args)

вот так оно сводилось к (более упрощенному для нашего понимания) и теперь, внимательно наблюдайте, list выполняет итерацию генератора, а не фактический генератор my_zip. Теперь list вызывает next объекта-генератора и оценивается arg[2], только чтобы найти, что 2 не является допустимым индексом для arg (который в этом случае равен [1, 2]), поэтому IndexError, и list не справляется с этим (у него нет причин справляться с этим в любом случае), и поэтому он терпит неудачу.


Как и в случае редактирования,

list(my_zip([1,2], ['a','b']))

будет оцениваться следующим образом. Сначала вызывается my_zip, и это даст вам объект-генератор. Затем повторите его с помощью list. Он называет next на нем, и он получает еще один объект-генератор list(arg[0] for arg in args). Поскольку не встречается никакого исключения или return, он вызывает next, чтобы получить еще один объект-генератор list(arg[1] for arg in args), и он продолжает итерацию. Помните, что полученные генераторы никогда не повторяются, поэтому мы никогда не получим IndexError. Вот почему код работает бесконечно.

Вы можете подтвердить это следующим образом:

from itertools import islice
from pprint import pprint
pprint(list(islice(my_zip([1, 2], ["a", 'b']), 10)))

и вы получите

[<generator object <genexpr> at 0x7f4d0a709678>,
 <generator object <genexpr> at 0x7f4d0a7096c0>,
 <generator object <genexpr> at 0x7f4d0a7099d8>,
 <generator object <genexpr> at 0x7f4d0a709990>,
 <generator object <genexpr> at 0x7f4d0a7095a0>,
 <generator object <genexpr> at 0x7f4d0a709510>,
 <generator object <genexpr> at 0x7f4d0a7095e8>,
 <generator object <genexpr> at 0x7f4d0a71c708>,
 <generator object <genexpr> at 0x7f4d0a71c750>,
 <generator object <genexpr> at 0x7f4d0a71c798>]

Таким образом, код пытается построить бесконечный список объектов генератора.

Ответ 2

def my_zip(*args):
    i = 0
    while True:
        try:
            yield (arg[i] for arg in args)
        except IndexError:
            raise StopIteration
        i += 1

IndexError не пойман, потому что (arg[i] for arg in args) - это генератор, который не выполняется немедленно, но когда вы начинаете повторять его. И вы перебираете его в другой области, когда вы вызываете list((arg[i] for arg in args)):

# get the generator which yields another generator on each iteration
gen = my_zip([1,2], ['a','b'])
# get the second generator `(arg[i] for arg in args)` from the first one
# then iterate over it: list((arg[i] for arg in args))
print(list(next(gen)))
  • В первом list(next(gen)) i равен 0.
  • На втором list(next(gen)) i равен 1.
  • На третьем list(next(gen)) i равно 2. И здесь вы получаете IndexError - во внешней области. Линия рассматривается как list(arg[2] for arg in ([1,2], ['a','b']))

Ответ 3

Извините, я не могу предложить последовательное объяснение, касающееся неспособности поймать исключение, однако, есть легкий путь вокруг него; используйте цикл for по длине кратчайшей последовательности:

def my_zip(*args):
    for i in range(min(len(arg) for arg in args)):
        yield (arg[i] for arg in args)

>>> gen = my_zip([1,2], ["a",'b','c'])
>>> print(list(next(gen)))
[1, 'a']
>>> print(list(next(gen)))
[2, 'b']
>>> print(list(next(gen)))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Ответ 4

Попробуйте заменить yield (arg[i] for ...) следующим.

for arg in args:
    yield arg[i]

Но в случае чисел, вызывающих исключение как 1[1], нет смысла. Я предлагаю заменить arg[i] только на arg.